If you are a frontend developer, there is a fair chance that you have used
Jedwatson's classNames package in your projects. In fact, classNames is the official replacement for classSet, which was originally shipped in the React.js Addons bundle. It's one of the most used packages in the world of React.
A simple usecase for classNames
For instance, if we needed to conditionally apply css classes to an element inside a React component based on the component state, we may do it like the following:
class EnhancedButton extends React.Component {
// ...
render () {
const btnClass = classNames({
'btn': true,
'btn-large': true,
'btn-primary': true,
'btn-pressed': this.state.isPressed,
'btn-over': !this.state.isPressed && this.state.isHovered
});
return <button className={btnClass}>{this.props.label}</button>;
}
}
The approach above is neat, but given that many of the classes are applied unconditionally, the pattern of setting them to true in the configuration object becomes rather redundant. To counter that redundancy, we may write something like the following:
const btnClass = classNames(
'btn',
'btn-large',
'btn-primary',
{
'btn-pressed': this.state.isPressed,
'btn-over': !this.state.isPressed && this.state.isHovered
}
);
Age of ES6 template literals
However, as ES6 or ES2015 template literals became supported across mainstream browsers, it became simpler to write the above snippet as:
const btnClass = `
btn
btn-large
btn-primary
${this.state.isPressed ? 'btn-pressed' : ''}
${(!this.state.isPressed && this.state.isHovered) ? 'btn-over' : ''}
`;
Though the template literal way of writing class names is somewhat simpler and faster, it's still fairly redundant and it's not free of certain pitfalls. For example, while writing ${(!this.state.isPressed && this.state.isHovered) ? 'btn-over' : ''}
, making sure that an empty string ''
gets added if the condition fails, is quite redundant and long. And the template literal does not remove extra/unnecessary whitespace and newlines from the output on its own.
For example, why not write something like ${!this.state.isPressed && this.state.isHovered && 'btn-over'}
? But there is a pitfall; if the condition resolves to true
, the btn-over
gets added, but if the condition resolves to false
, the string 'false'
gets added to the output. What if we write expressions that resolve to undefined
or null
or NaN
or anything similar ? Javascript would simply treat them as strings and move on. It goes without saying that there are plenty of such 'shoot in the foot' scenarios with this approach.
Moreover, what if we already have a valid configuration object and we want to reduce it to a classnames string ? There is no obvious way to do that directly using only the template literals, we may possibly do it like Object.keys(config).reduce(...)
, or we may use the classNames package to do it for us. Of course, using the classNames package is more performant, because the package is well optimized for this usecase.
But what if there was a way to write the above example like the following without having any behavioral pitfalls and without losing any performance,
const btnClass = secretSauce`
btn
btn-large
btn-primary
${this.state.isPressed && 'btn-pressed'}
${!this.state.isPressed && this.state.isHovered && 'btn-over'}
`;
Enter classd
classd
is the secretSauce you needed in the example above. It's tagged template based fast and modern classNames alternative that preserves all the awesome bits of classNames and augments it with more.
The classd
tag processes the interpolation values in the template literal according to the following specification.
- Strings and numbers are valid values and are added to the output.
- It drops
undefined
,null
,NaN
andboolean
values. - If the value is an Array or an Iterable, it flattens the value and recursively processes the elements.
- If the value is an Object or a Map, it drops keys associated with falsy values and adds the remaining keys to the output.
- If the value is a function, it calls the function and adds its return value if that's valid
- It removes all unnecessary whitespace.
Here are a few examples:
classd`foo bar`; // => 'foo bar'
classd`foo ${null && 'bar'}`; // => 'foo'
classd`foo-${true && 'bar'}`; // => 'foo-bar'
classd`${true} ${false}`; // => ''
classd`${{ foo: true, bar: false}}`; // => 'foo'
classd`${{foo: true}} ${{bar: true}} ${{baz: false}}`; // => 'foo bar'
classd`a ${[ 'b', 'c', false && 'd' ]}`; // => 'a b c'
classd`${['a', { b: 1, c: 0 }]}`; // 'a b'
classd` a b \n ${Array(10).fill(' ')} c`; // => 'a b c'
Installation and usage
The classd
package exports 4 functions:
-
classd
(Tag for template literals, default) -
classDedupe
(Tag for template literals) -
classdFn
(Variadic function, for compatibility, similar toclassNames
) -
classDedupeFn
(Variadic function, for compatibility, similar toclassNames/dedupe
)
The package is available on NPM can be installed using package managers like npm
and yarn
. It can also be pulled from CDN directly into your webpages.
Install using package manager
# via npm
npm install --save classd
# or Yarn
yarn add classd
Use in ES6 Modules
// ES6 import (default - classd tag for template literals)
import classd from 'classd';
// example use
const width = 1080;
const classes = classd`container padding-${{
lg: width > 1280,
md: width > 960 && width < 1280,
sm: width <= 960
}} margin-0 ${width > 960 && 'blue'} ${width < 960 && 'red'}`;
console.log(classes); // => 'container padding-md margin-0 blue'
// ES6 import any of the exported functions
import { classd, classDedupe, classdFn, classDedupeFn } from 'classd';
// example use (of classdFn)
const width = 1080;
const classes = classdFn ('container', {
'padding-lg': width > 1280,
'padding-md': width > 960 && width < 1280,
'padding-sm': width <= 960
}, (width > 960 && 'blue'), 'margin-0');
console.log(classes); // => 'container padding-md blue margin-0'
Use in Commonjs modules (Nodejs)
// commonjs require classd tag for template literals (default export)
const classd = require('classd').default
// commonjs require any of the exported functions
const { classd, classDedupe, classdFn, classDedupeFn } = require('classd');
// commonjs require classd module
const classd = require('classd'); // exports can be used as classd.classd, classd.classDedupe etc
Pull from CDN
<script src='https://cdn.jsdelivr.net/npm/classd@1.0/lib/index.js'></script>
<script type='text/javascript'>
const { classd, classDedupe, classdFn, classDedupeFn } = window.classd;
console.log(classd`container ${1 > 0 && 'blue'}`); // => 'container blue'
</script>
Well, what are classDedupe
, classdFn
and classDedupeFn
?
The classdFn
follows the same specifications as the classd
tag. It's a straightforward replacement for classNames
. Everything that's valid with classNames
is also valid with classdFn
. In addition, classdFn
supports passing Maps, Sets, and other Iterables as arguments. Moreover it's slightly faster than classNames
in general usage.
If you want to migrate an existing project from using classNames
to classd
, using the classdFn
is the fastest and simplest thing to do. The migration from classNames
is as simple as:
// before
import classNames from 'classnames';
//after
import { classdFn as classNames } from 'classd';
The classDedupe
tag is an enhanced and about 60% slower version of the classd
tag. It does everything that the classd
tag does. In addition to that it checks for repeating names among the class names and ensures that each valid class name appears only once in the output string.
The classDedupeFn
is the function equivalent of the classDedupe
tag. It follows the same signature as classdFn
and classNames
.
It differs from the classNames/dedupe
in the behaviour that, the classNames/dedupe
unsets a class if a configuration object appearing later in its arguments unsets it; whereas classDedupe
does not unset a class name once it's set.
What about performance and stability ?
As conditionally applying class names is a common task in web frontend, and the functions are supposed to be called many times during a render cycle, it's imperative that the implementation of classd
be highly performant and stable. Therefore we take the stability and performance of this package very seriously. Updates are thoroughly reviewed for performance impacts before being released. We maintain a comprehensive test suite to ensure stability.
Here is a JSPerf benchmark of the classd
package, compared against classNames
. As we can see, the classd
tag is as performant as classNames
, while the classdFn
is slightly faster.
Source code and contributing
The source code is available on Github for you. Any contributions in the form of Pull Request, Issue or Suggestion are welcome. If you like it, please give it a star on Github.
classd
A minimal ES6 utility to compose classnames
classd is a fast, minimal JavaScript(ES6) utility for composing class names
It builds on ideas and philosophy similar to that of JedWatson's classnames
classd defaults to the idea of using ES6 template literals for composing class names.
It also provides functions similar to classNames
and classNames/dedupe
for
compatibility (with a minor behavioural difference in case of classNames/dedupe
detailed in a subsequent section).
It exports 4 functions:
-
classd
(Tag for template literals, default) -
classDedupe
(Tag for template literals) -
classdFn
(Variadic function, for compatibility, similar toclassNames
) -
classDedupeFn
(Variadic function, for compatibility, similar toclassNames/dedupe
)
Installation
# via npm
npm install --save classd
# or Yarn (note that it will automatically save the package to your `dependencies` in `package.json`)
yarn add classd
Use with ES6 modules (import)
// IMPORTING IN ES6
///////////////////
//
…Thanks for reading and do give it a try !
Top comments (0)