npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

downstyle

v0.1.1

Published

Minimal dependency theming system for React component authors

Downloads

6

Readme

Downstyle v0.1.0

Minimal dependency theming system for React component authors

downstyle adjective

  1. (typography, capitalization) Which only capitalizes words (e.g. in headers) that would be capitalized in a normal sentence.

Intro

While building Downright I realised I would need to provide a way for other users of the component to customise how it looked. As a component author you will always want to provide a decent default look and feel so the component "just works" right out of the box; but as we all know, once we encounter real-life concerns, no component is ever going to be "just right" as-is. The font needs to change, the colours are wrong, basically you might have heard of this thing called "branding".

Unfortunately React, despite being great at a great many things, really doesn't seem to have such a fantastic story when it comes to this all-too-common requirement. I could really find a comprehensive approach to this after quite some searching.

Of course this is a problem that has been solved by CSS since forever. You can ship your component with a stylesheet and use BEM classnames, and a developer can use global CSS to target any element they wish. This certainly gives a lot of flexibility, but is it good enough, and is it even the right approach?

Importantly, you want to leave control in the hands of your consumers: there are big advantages to using the hashed classNames that many React styling systems provide. Some users might prefer to just use inline styles. And there is another, harder, requirement that comes up now and again: what if the user needs to customise the actual HTML output of your component? CSS can sometimes solve this in a very rudimentary fashion with pseduo-elements like ::before and ::after, but sometimes requirements go beyond this. Some authors might provide additional wrapping elements to allow more complex styling, but this bloats your component. And failing that, your only solution is to fork the component and alter its rendering, or just roll your own version from scratch.

Downstyle is a system which will overcome all of these problems, allowing you to author components in such a way that the rendering and styling can be customised in granular detail.

Example

For a simple example we'll look at a very basic "FormField" component. It's a simple wrapper around a label and a text input. It might look something like this:

const FormField = ({className, children, id, ...others}) => (
    <div className={className}>
        <label for={id}>{children}</label>
        <input id={id} type="text" {...others}></input>
    </div>
);

Well, this isn't too bad - we can pass in a class, and target the inner elements with .className > input. But, this seems fragile. A later version of the component might change its internal structure and break our style.

Unfortunately, what happened next was my designer spent a week coming up with a truly beautiful design for this component, and we realised we needed an extra wrapping div around both the label and the input.

Downstyle provides a pretty simple helper method.

// Default rendering blocks
const defaultWrapper = ({children, ...others}) => <div {...others}>{children}</div>;
const defaultLabel = ({children, ...others}) => <label {...others}>{children}</label>;
const defaultInput = ({children, ...others}) => <input {...others}>{children}</input>;
class FormField extends Component {
    render() {
        const { theme, id, ...others } = props;
        if (!this.WrapperElement) {
            // Apply theme for each block
            this.WrapperElement = themed(defaultWrapper, theme, "wrapper");
            this.LabelElement = themed(defaultLabel, theme, "label");
            this.InputElement = themed(defaultInput, theme, "input");
        }
        const { WrapperElement, LabelElement, InputElement } = this;
        return (
            <WrapperElement>
                <LabelElement for={id}>{children}</LabelElement>
                <InputElement id={id} type="text" {...others} />
            </WrapperElement>
        );
    }
}

themed is a very simple wrapper (HOC) designed to be used around simple components - ones doing very little except emitting HTML components, i.e. presentational components. It takes anything passed in by the theme (an object consisting of classNames, styles, and elements), merges it together based on the name(s) provided, and hands you back new presentational components that can be customised.

The themed() helper itself is pretty small and adds minimal weight to your component. There are no additional external dependencies.

Note, the components must be created once and then re-used. Not doing so (i.e. creating them again on each Render method) is bad for performance and will actually break transitions, as React can't tell they're the same elements it creates all the DOM again from scratch.

However this means that your component won't respond if the theme is changed, so you should check for this in componentWillReceiveProps:

    componentWillReceiveProps(nextProps) {
        if (nextProps.theme !== this.props.theme) {
            this.WrapperElement = null;
            this.LabelElement = null;
            this.InputElement = null;
        }
    }

There is an annoying amount of boilerplate here; in a future version an additional HOC will be provided to make it easier to get the themed elements into your component. However it's still a pretty small amount of boilerplate for a huge gain in functionality!

Let's see what a developer can now do with the component, do help her/his CSS designers:

import { FormField } from "react-form-stuff";
import myFormStyles from "./MyFormField.css";
const theme = {
    elements: {
        label: (props) => {
            <div className={myFormStyles.labelWrapper}>
                <label {...props} className={myFormStyles.label} />
            </div>
        },
        input: (props) => {
            <div className={myFormStyles.inputWrapper}>
                <input {...props} className={myFormStyles.input} />
            </div>
        }
    }
}

const MyFormField = ({...props}) => {
    <FormField theme={theme} {...props} />
}

export default MyFormField;

So here, we've completely taken over the rendering of these two components, using our own CSS module which our designer created, yet retaining any and all functionality that might be baked into the form field by passing along {...props}. Of course instead of this, I could have done something like use styled.div`` from styled-components to render these elements.

Let's look at another scenario and how we might approach that. We'll add some new functionality to our FormField: it now has render states for focus, success, and error. I'm going to rewrite the original component a little bit, so it uses its own CSS modules:

import classNames from "./FormField.css";
class FormField extends Component {
    onFocus() {
        this.setState({focus: true});
    }
    onBlur() {
        this.setState({focus: false});
    }
    render() {
        const {children, id, success, error, ...others} = this.props;
        const {focus} = this.state;
        return (
            <div className={`${classNames.wrapper} ${focus?classNames.focus:''} ${error?classNames.error:''} ${success?classNames.success:''}`}>
                <label for={id} className={classNames.label}>{children}</label>
                <input id={id} className={classNames.input} type="text" {...others} onFocus={this.onFocus} onBlur={this.onBlur}></input>
            </div>
        );
    }

);

I won't show the whole CSS, but you can imagine things like:

.wrapper.focus {
    background: lightblue;
}

.wrapper.error {
    background: lightred;
}

.wrapper.error > .input {
    border: 2px solid red;
}

We can also do nice transitions between these states.

Now to support theming, we have a lot of classNames to juggle! Also if we want to support inline styles, we have loads of style objects to merge together, various classNames to conditionally concatenate, and we haven't even supported passing in a normal className prop for per-field variations.

Luckily, Downstyle has a really simple solution for this. When creating your themed elements, there is a 4th parameter called "mapPropsToTheme". This enables exactly this kind of scenario. The new component would be rewritten as follows:

// Default theme
import classNames from "./FormField.css";
const defaultTheme = { classNames };
// Default rendering blocks
const defaultWrapper = ({children, ...others}) => <div {...others}>{children}</div>;
const defaultLabel = ({children, ...others}) => <label {...others}>{children}</label>;
const defaultInput = ({children, ...others}) => <input {...others}>{children}</input>;
class FormField extends Component {
    static defaultProps = {
        theme: defaultTheme
    }

    render() {
        if (!this.WrapperElement) {
            // Apply theme for each block
            const mapPropsToTheme = (prefix = "") => ({
                focus: `${prefix}focus`,
                success: `${prefix}success`,
                error: `${prefix}error`
            });
            this.WrapperElement = themed(defaultWrapper, theme, "wrapper", mapPropsToTheme());
            this.LabelElement = themed(defaultLabel, theme, "label", mapPropsToTheme("label--"));
            this.InputElement = themed(defaultInput, theme, "input", mapPropsToTheme("error--"));
        }
        const { theme, success, error, ...others } = props;
        const { focus } = this.state;
        const states = { focus, success, error };
        const { WrapperElement, LabelElement, InputElement } = this;
        return (
            <WrapperElement {...states}>
                <LabelElement {...states} for={id}>{children}</LabelElement>
                <InputElement {...states} id={id} type="text" {...others} onFocus={this.onFocus} onBlur={this.onBlur} />
            </WrapperElement>
        );
    }
}

Now, with very little code added, we have some powerful new functionality - the entire component can be modified freely depending on its current state.

This is the basic intro. If you find this useful, please let me know, and especially if find there are any requirements not covered by this system.

For more examples of this being used in production, see the "Styling" example in Downright, where the menus are being customised using this system: https://github.com/downplay/downright/blob/master/examples/source/examples/Styling.jsx

Version History

0.1.1

  • Properties specified in mapPropsToTheme are no longer passed to the rendered element
  • TODO: In some cases might an author might want these props available? Maybe add an option for this.

0.1.0

  • First release

Copyright

©2017 Downplay Ltd

Distributed under MIT license. See LICENSE for full details.