1.1.0 • Published 5 years ago

@n_bryant/classnames-helper v1.1.0

Weekly downloads
1
License
UNLICENSED
Repository
github
Last release
5 years ago

classnames-helper

A support library for achieving extensible styling for reusable components and setting BEM semantic class names for component identification. This library provides a companion tool for React components that eases the burden of implementing a component's CSS API in tandem with BEM identifiers.

The goal of this "classnames helper" is to provide a tool that removes the need to employ multiple helper libraries to build class names (eg. classnames and bem-helper-js) by selecting classes from a component's CSS API (props.classes) and directly associating each with BEM identifiers and modifiers.

Getting Started

  # via npm
  npm install @n_bryant/classnames-helper

  # via yarn
  yarn add @n_bryant/classnames-helper

Usage

Recommended Usage With React

The helper expects to receive a component's name, and the helper should be attached to the component statically.

MyComponent.classnames = createClassNameGenerator("MyComponent");

Next, the helper must be applied with props to build the prop specific class name helper. The helper expects a classes prop within the props object passed to it.

function MyComponent(props) {
  const classnames = MyComponent.classnames(props);
}

Lastly, the class name helpers can be applied to the root or sub elements with optional modifiers.

function MyComponent(props) {
  const classnames = MyComponent.classnames(props);
  return (
    <div className={classnames.root({ isFoo: true })}>
      <span className={(classnames.element("mySpan"), { isBar: true })}>
        Span Content
      </span>
    </div>
  );
}

Complete Example

import React from "react";
import PropTypes from "prop-types";

import { withStyles } from "@material-ui/core/styles";
import createClassNameHelper from "classnames-helper";
import theme from "../lib/theme";

import Drawer from "@material-ui/core/Drawer";
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";

export function ExampleComponent(props) {
  const { classes, open, onClose, title, children } = props;
  const classnames = ExampleComponent.classnames(classes);
  return (
    <Drawer {...props} className={classnames.root({ isOpen: open })}>
      {/** */}
      <div className={classnames.element("title")}>
        <Button
          onClick={onClose}
          className={classnames.element("titleCloseButton")}
        >
          <Typography className={classnames.element("titleCloseButtonText")}>
            Close
          </Typography>
        </Button>
        <Typography
          className={classnames.element("titleText", {
            isHighlighted: open
          })}
        >
          {title}
        </Typography>
      </div>
      {children}
    </Drawer>
  );
}
ExampleComponent.classnames = createClassNameGenerator("ExampleComponent");
ExampleComponent.propTypes = {
  classes: PropTypes.shape({
    /** Applied to the root element */
    root: PropTypes.string,
    /** Modifier applied to the root element when the Drawer is open */
    isOpen: PropTypes.string,
    /** Applied to the title container, wrapping the text and close button */
    title: PropTypes.string,
    /** Applied to the title text */
    titleText: PropTypes.string,
    /** Applied to the title text when the Drawer is open and the title gets highlighted*/
    titleTextIsHighlighted: PropTypes.string,
    /** Applied to the close Button in the title container */
    titleCloseButton: PropTypes.string,
    /** Applied to the close button text inside the Button in the title container */
    titleCloseButtonText: PropTypes.string
  }),
  /** Whether the Drawer is open or not */
  open: PropTypes.bool,
  /** The Drawer's title */
  title: PropTypes.string,
  /** Handler for when the Drawer is closed */
  onClose: PropTypes.func
};
ExampleComponent.defaultProps = {
  classes: {},
  open: false
};

// styles set here with property names providing the keys of the classes object
const styles = {
  root: {},
  isOpen: {},
  title: {},
  titleText: {},
  titleTextIsHighlighted: {},
  titleCloseButton: {},
  titleCloseButtonText: {}
};

export default withStyles(styles)(ExampleComponent);

Raw API

declare function rootClassNameHelper(modifiers?: object): string;
declare function elmentClassNameHelper(elementName: string, modifiers?: object): string;

interface PropsWithClasses {
  className?: string;
  classes?: object
}

interface ClassNameHelper {
  root: rootClassNameHelper;
  element elmentClassNameHelper;
}

interface BEMHelperWaitingForProps extends ClassNameHelper {
  (props: object) => ClassNameHelper
}

declare function createClassNameHelper(
  componentOrName: string | function,
  props: PropsWithClasses
): ClassNameHelper;
declare function createClassNameHelper(
  componentOrName: string | function
): BEMHelperWaitingForProps;

Construction

To create a class name helper, the factory createClassNameHelper will need a component name and props that contain the optional properties className and classes (for a component's CSS API). If props are not supplied, the component name will be derived.

import createClassNameHelper from "classnames-helper";
const props = {
  className: "ClassNameFromProp",
  classes: {
    root: "rootFromClasses",
    subElement: "subElementFromClasses"
  }
};
const classnames = createClassNameHelper("MyComponent")(props);
const rootClassNames = classnames.root();

Root Element

The helper will select props.className and props.classes.root from the component's props as well as provide a BEM block identifier based on the component's name.

const classnames = createClassNameHelper("MyComponent")({
  className: "ClassNameFromProp",
  classes: {
    root: "rootFromClasses"
  }
});
const rootClassNames = classnames.root().split(" ");
expect(rootClassNames).toContain("MyComponent");
expect(rootClassNames).toContain("ClassNameFromProp");
expect(rootClassNames).toContain("rootFromClasses");

Sub Elements

The helper can produce BEM sub element identifiers and draw element class names from props.classes as well.

const classnames = createClassNameHelper("MyComponent")({
  classes: {
    subElement: "subElementFromClasses"
  }
});
const elementClassNames = classnames.element("subElement").split(" ");
expect(elementClassNames).toContain("MyComponent__subElement");
expect(elementClassNames).toContain("subElementFromClasses");

Modifiers

The helper can produce BEM modifier identifiers and conditionally draw modifier class names from props.classes. When modifying sub elements, this expects a camelCase naming convention for properties in classes equivalent to _.camelCase([elementName, modifier]).

const classnames = createClassNameHelper("MyComponent")({
  classes: {
    isOpen: "rootIsOpenModifier",
    subElementSelected: "subElementIsSelectedModifier"
  }
});
const rootClassNames = classnames.root({ isOpen: true }).split(" ");
expect(rootClassNames).toContain("MyComponent");
expect(rootClassNames).toContain("MyComponent--isOpen");
expect(rootClassNames).toContain("rootIsOpenModifier");
// When the modifier condition is falsy, the modifier class names will not get added
expect(classnames.root({ isOpen: false }).split(" ")).not.toContain(
  "MyComponent--isOpen"
);
expect(classnames.root({ isOpen: false }).split(" ")).not.toContain(
  "rootIsOpenModifier"
);

const elementClassName = classnames
  .element("subElement", { selected: true })
  .split(" ");
expect(elementClassName).toContain("MyComponent__subElement");
expect(elementClassName).toContain("MyComponent__subElement--selected");
expect(elementClassName).toContain("subElementIsSelectedModifier");
// When the modifier condition is falsy, the modifier class names will not get added
expect(
  classnames.element("subElement", { selected: false }).split(" ")
).not.toContain("MyComponent__subElement--selected");
expect(
  classnames.element("subElement", { selected: false }).split(" ")
).not.toContain("subElementIsSelectedModifier");

Just BEM Identifiers

If you want to use the helper outside of a component, perhaps to generate BEM identifiers for a component in absence of props, simply invoke the helper without props.

const classnames = createClassNameHelper("MyComponent");
const rootClassNames = classnames.root({ isOpen: true }).split(" ");
expect(rootClassNames).toContain("MyComponent");
expect(rootClassNames).toContain("MyComponent--isOpen");