/react-classnaming

Tools to establish CSS classes as an explicit abstraction layer and to handle it as an interface between React and CSSStyleDeclaration

Primary LanguageTypeScriptMIT LicenseMIT

Tools to establish CSS classes as an explicit abstraction layer and to handle it as an interface between React and CSSStyleDeclaration.

build@ci codecov Quality Gate Status Maintainability Scrutinizer Code Quality DeepScan grade CodeFactor

dependencies Status version license

Objectives

  1. Use CSS classes as an ontology of front-end project for clean communication between developers and non-tech staff
  2. Make CSS classes be an explicit and predictable informational layer
  3. Enforce declarative programming paradigm
  4. Enforce contract programming (via TypeScript)

Dev features

  1. Enforce single source of truth of class appending – treat as TypeScript-driven dedupe
  2. Require strict boolean for value of class condition
  3. Use IDE type hints as developers' UX for faster issues resolving
  4. BEM
  5. CSS-modules agnostic

Use package like postcss-d-ts to prepare strict declaration of CSS

Installation and brief overview

npm install --save react-classnaming
import {
  // Returns function for building `className: string` from conditioned CSS classes with "context" (if was provided) from `props` for using only declared CSS classes
  classNaming, 
  
  // Similar to classNaming, specifies mapping to component's (i.e. 3rd-party) `className`-related props
  classNamesMap,
  
  // Identical function for TS restriction on classes determed in CSS and not used in component
  classNamesCheck,
  
  // Works with BEM conditional object
  classBeming
} from "react-classnaming"

// Default export is the most simple function
import classNaming from "react-classnaming"

import type {
  // Type to declare component's self CSS classes
  ClassNamesProperty, 
    
  // Type to gather required CSS classes of sub-components
  ClassNames, 
    
  // `= string | undefined` – type to declare CSS class, global or local
  ClassHash, 
    
  // `= {className: string}` – useful shortcut
  ClassNamed 
} from "react-classnaming"

Basic usage

Example of simple CSS classes conditioning – ./_tests_/readme.spec.tsx:9

import classNaming from "react-classnaming"

type Props = {
  isValid: boolean
  readOnly: boolean
}

// isValid = false, readOnly = false
function FormButtons({isValid, readOnly}: Props) {
  const cssClasses = classNaming()
  const buttonClass = cssClasses({"button": true}) // "button"

  return <>
    <button {
      ...buttonClass // className="button" 
    }>Close</button>
    <button type="reset" {
      ...buttonClass({"button--disabled": readOnly}) // className="button"
    }>Reset</button> 
                     { /* className="button_submit button button--disabled" */ }
    <button type="submit" className={`button_submit ${
      buttonClass({"button--disabled": readOnly || !isValid}) // "button button--disabled"
    }`}>Submit</button> 
  </>
}  

As shown, producing function classNaming returns a multipurpose object. It can be

  • recalled to stack more CSS classes on conditions: anotherClass = someClass({...})({...})
  • destructed in component's props as className singleton: <div {...someClass}/><button {...anotherClass}/>
  • used as a string: ``${someClass} ${anotherClass}``

Demos

You can find demonstration with all main points in folder ./_examples_/, in addition *.test.* and *.spec.*. <img src="./images/vscode.png" width="50%" align="right/>

Getting more

Condition is strictly boolean

Conditions with falsy values may lead to hardly caught bugs due to not obvious behavior for humans. In addition, as a possible true shortcut, the value can be not empty string as class-hash from CSS-module, and undefined for global CSS-class or modules simulation. Thus, to not keep in mind that undefined appears to be a truthy condition, it is prohibited on TypeScript level to mix in value type boolean with ClassHash = string | undefined and not allowed to use any other types like 0, null. ./_tests_/readme.spec.tsx:43

Single source of truth

There can be only ONE condition for each class in call pipe. Already conditioned classes are propagated to next call type notation so you can see currently stacked with according modality: true, false or boolean. ./_tests_/readme.spec.tsx:55

classnaming_single_truth

Declare own component's CSS classes

Only declared CSS classes will be allowed as keys with IDE hint on possibilities – ./_tests_/readme.spec.tsx:71

+ import type { ClassHash, ClassNamesProperty } from "react-classnaming"

+ type MyClassNames = ClassNamesProperty<{
+   button: ClassHash
+   button_submit: ClassHash
+   "button--disabled": ClassHash
+ }>

- const cssClasses = classNaming()
+ const cssClasses = classNaming<MyClassNames>()

classnaming_declared

BEM

It is possible to use BEM as condition query. With explicitly declared CSS classes (i.e. via postcss-plugin-d-ts) TS and IDE will check and hint on available blocks, elements, modifiers and values. ./_tests_/readme.spec.tsx:165

import {
- classNaming 
+ classBeming
} from "react-classnaming"

- const cssClasses = classNaming<MyClassNames>()
+ const bemClasses = classBeming<MyClassNames>()

Reference

type ClassNamed

Shortcut for {className: string}.

type ClassHash

For serving global and local CSS classes and not moduled CSS stylesheets. CSS-module will be imported as {[cssClasses: string]: string}, while for ordinary CSS import require returns just empty object {}. Their common notation is {[cssClasses: string]: string | undefined} , thus type ClassHash = string | undefined

function classNaming

Sets context for further type checks in supplying and toggling.

classNaming()
classNaming<MyProps>()
classNaming<MyClassNames>()
classNaming({classnames: require("./some.css")})
classNaming({classnames: module_css, className})
classNaming(this.props)

Returns pipe-able (recallable) callback, that also can be destructed as ClassNamed or stringifyed

const cssClasses = classNaming(...)
const btnClass = cssClasses({ button })

return                               
  <div {...btnClass } />
  <div data-block={`${btnClass}`} />
  <Component {...{
    ...btnClass(...)(...)(...)}
  }/>

On TS-level checks that Component's propagated className and certain CSS-class are conditioned once

const conditionForClass1: boolean = false
const containerClass = classes(true, {class1: conditionForClass1})

const withClass1Twice = containerClass({
  class2: true,
  //@ts-expect-error – TS tracks that in chain there's only 1 place for class to be conditionally included 
  class1: otherCondiition
})

const withClassNameTwice = containerClass(
  //@ts-expect-error - Same for `className` - it is already added
  true
)

On const hovering will be tooltip with already conditioned classes under this chain

function classBeming

Sets context to returned function for using BEM conditioned CSS classes queries. General argument's shape is

// .src/bem.types.ts#L84-L90
type BemInGeneral = {
  [base: string]: undefined | boolean | string
  | (false|string)[]
  | {
    [mod: string]: undefined | boolean | string
  }
}

Output logic: ./src/bem.core.test.ts:13

Featured example: ./_tests_/readme.spec.tsx:191


Setting options

Default options BEM naming:

  • Modifier's and value's separator is a double hyphen "--"
  • Element's separator is a double underscore "__"

It is required to change this options twice, both on JS and TS levels.

/// <reference types="react-classnaming" />
declare namespace ReactClassNaming {
  interface BemOptions {
    elementDelimiter: "_";
    modDelimiter: "-";
  }
}

And optionally in add to tsconfig.json:

  "compilerOptions": {
    "types": [
+     "react-classnaming"
    ]
  }

function classNamesMap

Function to map classnames to string props of some (i.e. 3rd-party) component.

const { Root } = classnames
const mapping = classNamesMap(classnames)

<ThirdPartyComponent {...mapping({} as typeof ThirdPartyComponent, {
  ContainerClassName: { Root, "Theme--dark": true },
  Checked___true: classes({ "Item--active": true }),
  Checked___false: {}
})} />

For hint will be used such props of target component that can be assigned to string. After calling mapping function and setting other properties, usual TypeScript will check for presence of target's required properties and other ordinary for TS things.

Declaration of self Component's classnames

  type MyClasses = ClassNamesProperty<{
    class1: ClassHash
    class2: ClassHash
  }>

Can be restricted to use classes only from CSS module. Note Currently no IDE's tooltip for hints

  type MyProps = ClassNamesProperty<
    typeof some_module_css,
    //@ts-expect-error
    {class1: ClassHash, class2: ClassHash, unknownClass: ClassHash}
  >

Collects/gathers required classnames from used sub-Components

type MyProps = ClassNames<true> // === ClassNamed === {className: string}
type MyProps = ClassNames<Props> // {classnames: Props["classnames"]}
type MyProps = ClassNames<typeof Component>
type MyProps = ClassNames<true, Props, typeof ClassComponent, typeof FunctionalComponent>
type Props = ClassNames<true, Sub1Props, typeof Sub2>
  
function Component({className, classnames, "classnames": {Sub1Class}}: Props) {
  const classes = classNaming({classnames, className})
  
  return <div>
    <Sub1 {...classes(true, {Sub1Class})} classnames={classnames}/>
    <Sub2 {...{
        ...classes({Sub2Class: true}),
		classnames
    }}/>
  </div>
}

type ClassNamesFrom

Obtain classnames-object from props of functional component, class component or props type

ClassNamesFrom<ComponentProps>;
ClassNamesFrom<typeof Component>;

Identical function or returning constant EMPTY_OBJECT for keys check of not used classes in components tree

import css from "./page.scss"
import App from "./App.tsx"

ReactDOM.render(<App classnames={classNamesCheck(...)} />
  • Dummies shape
<Component classnames={classNamesCheck()} />;
  • Checks CSS with defined (not indexed) classes keys. To produce such declaration you can use package postcss-plugin-d-ts.
import type { ClassNamesFrom } from "react-classnaming/types";
import css_module from "./some.css"; // With class `.never-used {...}`

<Component classnames={classNamesCheck(
  css_module, 
  //@ts-expect-error Property 'never-used' is missing
  {} as ClassNamesFrom<typeof Component>
)} />;

Misc

Restructuring

Using CSS-modules or simulation

It is possible to use CSS modules or simulation without "context" by supplying class-hash value with variable ./_tests_/readme.spec.tsx:114

// CSS-module, assuming "button" will be replaced with "BTN"
+ import css_module from "./button.module.css"
+ const { button } = css_module
// Module simulation
+ type CssModuleSimulation = { button_submit: ClassHash }
+ const { button_submit } = {} as CssModuleSimulation
  
  type MyClassNames = ClassNamesProperty<
+   typeof css_module &
+   CssModuleSimulation & 
    {
-     button: ClassHash
-     button_submit: ClassHash
      "button--disabled": ClassHash
    }
 >

- const buttonClass = cssClasses({ button: true })
+ const buttonClass = cssClasses({ button })

  <button type="submit" {...buttonClass({
-    "button_submit": true, 
+    button_submit,
    "button--disabled": readOnly || !isValid
  })}>Submit</button>  

Versus classnames package

See src/versus-classnames.test.ts

//TODO Copy here the most significant TS errors

No css-modules, just simulation

import classnames from "classnames"
<div className={classnames("class1", "class2")} />
<div id={classnames("class1", "class2")} />

// VERSUS

import css from "./some.css"
import classNaming, {classNamesCheck} from "react-classnaming"
import type {ClassNames} from "react-classnaming"

const { class1,
  //@ts-expect-error
  whatever
} = classNamesCheck<...>(css)

const props: ClassNames<"class2"> = {"classnames": css}

const {class2} = props.classnames

<div {...classNaming({class1, class2})} />
<div id={`${classNaming({class1, class2})}`} />

CSS module

import module_css from "./some.module.css" // {"class1": "hash1", "class2": "hash2"}

import classnames_bind from "classnames/bind"
const cx = classnames_bind.bind(module_css)
// No error on redundant CSS-class
<div className={cx("class1", {"class3": true})} />

// VERSUS

import classNaming from "react-classnaming"
const clases = classNaming({classnames: module_css})
//@ts-expect-error Argument of type '"class3"' is not assignable to parameter
<div {...clases({class1: true, class3: true})} />