react-classifier v0.2.1
react-classifier
A library for adding classes to a tree of React elements using CSS-like selectors, so you can turn this
const Card = ({ title, imgSrc, texts }) => (
<div className="card mb-3">
<img className="card-img-top" src={imgSrc} alt="Card image" />
<div className="card-body">
<h5 className="card-title">{title}</h5>
{texts.map((text, i, all) =>
<p key={i} className={`card-text ${i === all.length - 1 ? 'small text-muted' : ''}`}>
{text}
</p>
)}
</div>
</div>
)into this
import C, { lastChild } from 'react-classifier'
const Card = ({ title, imgSrc, texts }) => C(
<div>
<img src={imgSrc} alt="Card image" />
<div>
<h5>{title}</h5>
{texts.map((text, i) =>
<p key={i}>
{text}
</p>
)}
</div>
</div>, {
':root': ['card mb-3', {
'img': 'card-img-top',
'div': ['card-body', {
'h5': 'card-title',
'p': ['card-text', lastChild('small', 'text-muted')]
}]
}]
}
)(Example card component from http://getbootstrap.com/docs/4.0/components/card/#image-caps)
That's it! It simply allows you to "separate concerns" (visually, at least), uncluttering your component markup by keeping the classes in a separate structure, while also providing a few convenient helpers.
Motivation
There exist solutions like CSS Modules, styled-components, glamor, or styled-jsx, with alternatives for styling a React component (some of these libraries are not React-specific, though) while keeping the CSS rules scoped.
This library instead attempts to be of use for working with class-heavy, utility-based frameworks like Bootstrap or Tailwind CSS. With it, the className property can be set not inline, but by means of a separate tree-like structure, which may partially mirror the "markup", that is, the tree of React elements. This can allow, as needed, reusing / merging / composing this classes map, which can also be imported from a separate file, etc.
Installation
As usual,
yarn add react-classifieror
npm install --save react-classifierUsage
The main function should receive a tree of React elements as first argument, and an object as second argument (the default is an empty object).
This object has a subset of CSS selectors as keys, and a string, array, object or function, as values (details below).
Selectors
A subset of CSS selectors are supported (can not be combined such as div.red)
Type ("tag") selector
Either a tag name such as
'div', or a React component type such as'MyComponent'(this would make sense if the element itself accepts aclassNameprop, or for a nested structure)Id selector
'#navbar'will match an element with anidprop with value 'navbar'.Class selector
'.red'will match elements already containing aclassNameprop with value 'red'.:rootselector':root'will match the top-level element.*selector'*'will match any element.
Class arguments
The object value is the class(es) to apply to the element(s) selected with the associated key.
This value can be:
- a string with the class to apply
- an object where each value is an expression that, if it evaluates as truthy, will apply the associated key as class name
- a function, which will be called with 3 arguments: the selected element, the (0-based) index of the element within its siblings, and the array of said siblings. The return value will be processed as per the rules above.
- an array, which will be flattened and each of its element will be processed as per the rules above
In any case, any falsy values (empty string, undefined, etc.) will be automatically filtered out.
Examples
These examples with the default export aliased as C (import C from 'react-classifier')
Type selector, string class
const Component = () => C(
<div />, {
'div': 'red'
}
)renders as
<div className="red" />Class selector, object classes
const Component = () => C(
<div className="a" />, {
'.a': { b: true, c: 'truthy', d: 0 === 1 }
}
)renders as
<div className="a b c" />Note that the resulting classes will be merged with the existing component className, if any.
Id selector, array classes
const Component = () => C(
<div id="navbar" />, {
'#navbar': ['flex', 'bg-red', 'text-dark']
}
)renders as
<div id="navbar" className="flex bg-red text-dark" />Mixed array
const Button = (props) => C(
<button />, {
':root': [
'btn',
{
'btn-danger': props.danger,
'btn-warning': props.warning
},
['text-center', 'text-uppercase'],
(el, i) => ({ 'text-lg': i === 0 })
]
}
)A first-child <Button warning /> renders as
<button className="btn btn-warning text-center text-uppercase text-lg" />- Unlike a CSS selector, only top-level elements will be matched:
C(
<div>
<div />
<div />
</div>, {
'div': 'flex'
}
)renders
<div className="flex">
<div />
<div />
</div>However, If a selector matches no top-level element, the matching algorithm will traverse down the elements tree until a match is found:
C(
<div>
<p />
<p />
</div>, {
'p': 'paragraph'
}
)renders
<div>
<p className="paragraph" />
<p className="paragraph" />
</div>Nesting
There is a single exception to the rules above: if an entry value is an array and its last value is an object, this object will be used to select and apply classes on descendants of the current element:
C(
<div>
<p />
<p />
</div>, {
'div': ['flex', {
'p': 'paragraph'
}]
}
)renders
<div className="flex">
<p className="paragraph" />
<p className="paragraph" />
</div>See the top of this README for a more complex example.
Helper functions
The library provides nthChild, firstChild and lastChild as named exports. They all generate the corresponding classes if the element is nth, first, or last within its siblings.
function nthChild(n, ...classes) { /*...*/}
function firstChild(...classes) { /*...*/}
function lastChild(...classes) { /*...*/}C(
<div>
<p />
<p />
<p />
</div>, {
'div': [{
'p': nthChild(2, 'second', {'child': true})
}]
})renders
<div>
<p />
<p className="second child" />
<p />
</div>Component decoration
The decorate named export takes a component and another argument which will be passed to the :root of the wrapped component, returning a higher-order component.
const Button = (props) => (
<button className='btn'>
{props.children}
</button>
)
const RedButton = decorate(Button, 'bg-red')
<RedButton>A red button</RedButton>)renders
<button className="btn bg-red">A red button</button>Any structure can be passed to the decorate helper function, which allows adding classes to child elements:
const List = ({ items }) => (
<ul>
{items.map((item, i) =>
<li key={i}>{item}</li>
)}
</ul>
)
const StripedList = decorate(List, [{
li: [
'text-dark',
(_, i) => i % 2 && 'bg-light-grey'
]
}])
<StripedList items={['a', 'b', 'c', 'd']} />renders
<ul>
<li className="text-dark">a</li>
<li className="text-dark bg-light-grey">b</li>
<li className="text-dark">c</li>
<li className="text-dark bg-light-grey">d</li>
</ul>A word of caution
This should not (yet) be considered ready for production use. There may still exist bugs or performance issues. PRs and comments are welcome.
License
MIT © Alejandro Peinó