0.4.7 • Published 1 year ago

react-class-composer v0.4.7

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

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",
  }
);
0.4.7

1 year ago

0.4.6

1 year ago

0.4.5

1 year ago

0.4.4

2 years ago

0.4.3

2 years ago

0.4.2

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.3.2

2 years ago

0.3.1

2 years ago

0.3.0

2 years ago

0.2.3

2 years ago

0.2.2

2 years ago

0.2.1

2 years ago

0.1.4

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago

0.0.1

2 years ago