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

react-class-composer

v0.4.7

Published

Simple tool to compose css classnames based on component props

Downloads

121

Readme

React Class Composer

Simple tool to compose css classnames based on component props

Install

  $ npm install react-class-composer
  $ yarn add react-class-composer

Motivation

react-class-composer was built as a tool for creating low level basic building block components for new design systems or UI libraries that use utility-css frameworks to style components (like tailwind).

there are definitely other libraries that achieve this, and if you are looking to solve that problem and react-class-composer does not fit your needs, I encourage you to check them out: useFancy, use-utility-classes, React With Class


How does it work?

use createComponent() we can create and forward a native HTML component:

import { createComponent } from "react-class-composer";

type BoxProps = {
  display?: "flex" | "block" | "inline";
};

export const Box = createComponent<BoxProps>("div", {
  base: "box-base",
  options: {
    display: {
      flex: "display-flex",
      block: "display-block",
      inline: "display-inline",
    },
  },
});

Using the component:

  <Box display='flex'><Box>
  <Box display='inline'><Box>
  <Box display='block'><Box>

HTML Output:

<div className="box-base display-flex"></div>
<div className="box-base display-inline"></div>
<div className="box-base display-block"></div>

Hooks

useClassComposer() Hook

this hook acts like the createComponent function, but lets you deal with all the component outer shell. it returns a classname based on a config file, and requires a config and props object.

import React from "react";
import { useClassComposer } from "react-class-composer";

interface Props {
  size: "small" | "medium" | "large";
  something: React.ReactNode;
}

export const YourComponent: React.FC<Props> = (props) => {
  const { className } = useClassComposer<Props>(
    {
      base: "base-class",
      options: {
        size: {
          small: "small-class",
          medium: "medium-class",
          large: "large-class",
        },
      },
    },
    props
  );

  return (
    <div className={className}>
      your component
      {props.something}
    </div>
  );
};

useClassname() Hook

this hooks just compiles the @ClassDefinition object into memoized string of classnames. its kind of like clsx(). it takes a config object and optionally a React.DependencyList array.

import React from "react";
import { useClassname } from "react-class-composer";

const ComponentWithClass: React.FC = ({ props }) => {
  const className = useClassname(
    [
      "btn",
      "btn-something",
      { hover: "btn-hover" },
      () => (props.something ? "btn-something" : "btn-not"),
    ],
    props
  );

  return <button className={className}>click me!</button>;
};

the @ClassDefinition type

export type ClassDefinition = string
| ClassDefinition[];
| ((value) => ClassDefinition)
| { [key: string]: ClassDefinition }

anytime you can define a class, you can use any combination of the following values:

String

  options: {
      prop: {
        a: "simple",
        b: "multiple classes in the same string" // will be .split(" ") before parsing
      }
  }

Array

any array of @ClassDefinition values will be flattened and parsed.

  options: {
      prop: {
        a: ["simple", "array"],
        b: ["multi", ["level", "array"]],
        c: [() => "string", {obj: "string"}]
      }
  }

Functions

you can use functions to generate dynamic classnames. functions can return any valid @ClassDefinition excluding function

  options: {
      prop: {
        a: () => "some-class-name"
      },
      anotherProp: (value) => `prop${value}`
  }

Objects

any object beyond the first level (used to parse prop values) will be exploded into prefixed classes like key:value. all keys need to be string, but the value can be any @ClassDefinition

  options: {
      prop: {
        a: "a-value", // will apply `a-value` if <... prop="a" />
        b: "b-value" // will apply `b-value` if <... prop="b" />
        c: {hover: 'color-red'}// will apply `hover:color-red` if <... prop="c" />
      },

  }

Mixers

[TODO]

$ and $$ Prefixes

[TODO]


Full Example

(view Button.tsx) (view Tests)

import {
  createComponent,
  mixAddClass,
  mixFunction,
  mixRemoveClass,
} from "react-class-composer";

type ButtonProps = {
  size: "tiny" | "small" | "medium" | "large";
  variant?: "none" | "outline" | "filled";
  rounded?: boolean;
  anotherOption?: "on" | "off";
  dynamicOptions?: number;
} & Partial<{
  // alias:
  round: ButtonProps["rounded"];
  v: ButtonProps["variant"];
}>;

export const Button = createComponent<ButtonProps, "button">("button", {
  "button",
  {
    /**
     * Base: base classes, will always be applied
     */
    base: [
      "btn",
      { hover: ["btn-hover", "text-bold"] }, // {key: 'value'} pairs will be exploded into prefixed classes like `key:value`
      () => "btn-base", // you can use functions to return a string or @ClassDefinition object
    ],

    /**
     * Mix: mix functions allow for conditional class toggling based on the multiple props.
     */
    mix: [
      // mixAddClass will add classes if all the conditions are true
      mixAddClass(["size.tiny", "variant.outline"], "size-tiny-outline-mix"),

      // mixRemoveClass will remove a class if all the conditions are true
      mixRemoveClass(
        ["size.tiny", "variant.filled"],
        ["btn-base", { hover: "btn-hover" }, "hover:text-bold"]
      ),

      // mix functions support wild card checks:
      mixRemoveClass(["data-something.a"], ["btn-base"]),

      // you can run your own mix functions
      mixFunction(["anotherOption.*", "disabled.true"], (css) =>
        css.add("any-anotherOptions-disabled-true")
      ),

      // or simply just pass in a mix object.
      { when: ["type.reset"], run: (css) => css.add("btn-reset") },
      // you can match any prop, even if its a native HTML element one
      {
        when: ["formNoValidate.true"],
        run: (css) => css.add("form-no-validate"),
      },
    ],

    /**
     * Alias: prop shortcuts for other options
     */
    alias: {
      // v="outline" is interpreted as variant="outline"
      v: "variant",
      round: "rounded",
    },

    /**
     * Options: options are [key,value] pairs, where the key is the prop name
     */
    options: {
      size: {
        // you can use string
        tiny: "padding-tiny margin-tiny",

        // or any combination of string[]
        small: [
          "padding-medium",
          "margin-medium",
          ["text-medium", "font-something"],
        ],

        // also supports () => string
        medium: () => `medium-stuff class-returned-by-function`,

        // any object key will be parsed as "prefixed" class name
        large: {
          large: ["text", "font", "padding"],
          key: { abc: ["a", "b", "c"], num: ["n1", "n2", "n3"] },
        },
      },

      // use $ as a prefix to mark a prop as "native" (comes from the native HTML element we are extending)
      $type: {
        submit: "btn-submit",
      },

      variant: {
        none: "",
        outline: "bg-white-500 text-black border border-gray-400",
        filled: "bg-teal-200 text-white",
      },
      rounded: "rounded-2xl",
      anotherOption: {
        on: "option-on",
        off: "options-off",
      },

      // dynamic options via function
      dynamicOptions: (value) => {
        if (value < 50) return "less-than-50";
        if (value > 50) return "more-than-50";
        return ["value-is-50", "dynamic-options-50"];
      },

      // use $ as a prefix to mark a prop as "native" (comes from the native HTML element we are extending)
      $disabled: "btn-disabled",

      // use $$ as a prefix to apply classes if a prop is present, ignoring what value it has
      $$title: "btn-has-title",

      // we can also target data-attributes:
      "data-something": {
        a: "something-a",
        b: "something-b",
      },
    },
  },
  {
    // defaults, will apply classes as if <... variant="none">
    variant: "none",
  }
);