⚛️ 🖼
class-bound-components React components bound to class names. As simple as that. Without tagged template literals.
What it does
- Create component bound to one or more class name
- Apply class names based on boolean props, referred to as variants
- Offers shortcut members to wrap intrinsic elements such as
classBound.blockquote('my-blockquote')
- Extend existing class bound components with the modifiers
extend
,as
,withVariants
andwithOptions
- Strong TypeScript support: Allowed props restricted to those of the composed component and variant flags
Use babel-plugin-class-bound-components
to benefit from:
- Automatic inferring of display names like you're used to with regular React functional components
- Backwards compatibility with browsers not supporting ES6
Proxy
, but still being able to use theclassBound[JSX.IntrinsicElement]()
shorthand (e.g.,classBound.button('foo')
instead ofclassBound('foo', null, null, 'button')
)
styled-components
Why not While CSS-in-JS approaches like styled-components have gained a lot of attention in the last couple of years you might be in a position where you can't or don't want to move to CSS-in-JS
- You might be using an external CSS library like Bootstrap
- You might be converting an old codebase to React and just want to focus on component functionality instead of also migrating all of your CSS to CSS-in-JS
- You might as well just not like CSS-in-JS
Still, you might want to have React components that abstract away the internals of your style sheets, or you're even using TypeScript and want to benefit from static types for styling components instead of raw class name concatenation.
This is where class-bound-components
comes into play. It allows you to bind class names, be it global class name strings or even class names generated by css-modules
to be bound to components. class-bound-components
enables you to introduce an abstraction layer between style sheets and component usage that can also support a future migration from CSS-in-CSS to CSS-in-JS.
Example
import classBound from 'class-bound-components';
import './breadcrumb.css';
const Container = classBound('container');
const Breadcrumb = classBound.ol('breadcrumb');
const BreadcrumbItem = classBound.li('breadcrumb-item', { isActive: 'active' });
const BreadcrumbLink = classBound.a('breadcrumb-link');
const BreadcrumbContainer: React.FC<{ items: Item[]; activeId: number }> = ({ items, activeId }) => (
<Container>
<Breadcrumb aria-label="breadcrumb">
{items.map(item => {
<BreadcrumbItem key={item.id} isActive={item.id === activeId}>
<BreadcrumbLink href={item.url} target="_blank">{item.name}</a>
</BreadcrumbItem>
})}
</Breadcrumb>
</Container>
);
const BreadcrumbButton = classBound.as(BreadcrumbLink, 'button');
const VisitableBreadcrumbLink = classBound.withVariants(BreadcrumbLink, { isVisited: 'visited' });
const CustomBreadcrumbItem = classBound.extend(BreadcrumbLink, 'custom-breadcrumb-item', { isActive: 'custom-active' });
Contents
- Installation
- API
- Usage with CSS Modules
- Display Names
- TypeScript Support
- Ref Forwarding
- Changelog
- License
Installation
# With npm
npm install --save class-bound-components
# With yarn
yarn add class-bound-components
In both cases make sure you have react
as well as react-dom
added to your project.
API
Component Creation
classBound(options)
Creates a new ClassBoundComponent
from an options object with the following properties. All options are optional.
Name | Type | Description |
---|---|---|
className |
string or string[]
|
Classes that are applied to the base component without any condition |
displayName |
string |
Display name of the component created. This appears for instance in the React devtools. When omitted it's referred to as Anonymous |
variants |
Record<string, ClassValue> 1
|
Object mapping the name of a variant, i.e., the name of the prop that has to be set to enable the variant, to a ClassValue that should be applied when the variant is enabled. |
elementType |
React.ElementType<any> |
Type of element to use a the base for the component. May be any string recognized by ReactDOM or a custom React component. default: 'div'
|
1 ClassValue
refers to any kind of value that can be passed into the classnames
Function.
const Button = classBound({
className: 'custom-button',
displayName: 'Button',
variants: { isPrimary: 'primary', isCTA: ['secondary', 'cta'] },
elementType: 'button'
});
classBound[JSX.IntrinsicElement](className[, displayName[, variants]])
Alias for classBound(options)
offering a member on the classBound
function for all known intrinsic elements, i.e., leaf elements that are recognized by React DOM.
Note that these shortcut members make use of the JavaScript Proxy
object. Using this in a browser that does not support Proxy
will throw a runtime error. If you need to support such browsers, it is recommended to make use of the babel-plugin-class-bound-components
, which will inline these method calls to the standard elementType
argument and hence won't use Proxy
anymore.
const CustomLink = classBound.a('custom-link', 'CustomLink', {
isActive: 'active',
});
const CustomQuote = classBound.blockquote('custom-quote');
classBound(className[, displayName[, variants[, elementType]]])
Alias for classBoundComponent(options)
containing all options defined above as positional arguments.
const Button = classBound('custom-button', 'Button', { isPrimary: 'primary' }, 'button');
const UnnamedButton = classBound('custom-button', { isPrimary: 'primary' }, 'button');
classBound(className[, variants[, elementType]])
Alias for classBoundComponent(options)
omitting the displayName
option which will be set to undefined
when calling this signature.
const Button = classBound('custom-button', { isPrimary: 'primary' }, 'button');
Button.displayName === undefined; // Meh, not interested in `displayName`
Modifiers
Modifiers are functions with which clones of an existing ClassBoundComponent
can be created with slight modifications. The modifiers extend
, withVariants
, withOptions
and as
are accessible as members of the classBound
function. Additionally, all of them can be imported as named imports from class-bound-components
import classBound, {
extend,
withVariants,
withOptions,
as,
} from 'class-bound-components';
classBound.extend === extend;
classBound.withVariants === withVariants;
classBound.withOptions === withOptions;
classBound.as === as;
classBound.extend(ClassBoundComponent, className[, displayName][, variants])
Extends an existing ClassBoundComponent
with class names and variants so that class names and already existing variants are combined. Useful when existing class names and variant class names should persist while augmenting them with more specific classes. The displayName
argument can optionally be left out.
const Button = classBound.button('button', 'Button', {
isActive: 'button-active',
});
const CustomButton = classBound.extend(
Button,
'custom-button',
'CustomButton',
{
isActive: 'custom-button-active',
}
);
<CustomButton isActive />;
// renders <button className="button custom-button button-active custom-button-active" />
classBound.as(ClassBoundComponent, elementType)
Creates a copy of a ClassBoundComponent
with similar options except the elementType
being set to a different value
const CustomButton = classBound.button('custom-button', 'CustomButton', { isPrimary: 'primary' });
// Oops need the same styles as an `<a />` tag
const CustomLink = classBound.as(CustomButton, 'a');
<CustomLink href="https://example.com/" target="_blank" isPrimary>Click me!</CustomLink>
// ^ awesome! TypeScript allows these <a> specific props now!
classBound.withVariants(ClassBoundComponent, mergeVariants)
Creates a copy of a ClassBoundComponent
with similar options except the variants
are merged with mergeVariants
. While old variants that are not specified in the merge variants remain untouched, naming conflicts are resolved by preferring the variants in mergeVariants
. Note that this differs from the behavior of ClassBoundComponent.extend
.
// button.tsx
import './buttons.css';
const BaseButton = classBound.button('baseButton', 'BaseButton', { isPrimary: 'primary', isFlashy: 'flashy' });
// my-custom-container.tsx
import 'my-custom-container.css';
const CustomButton = classBound.withVariants(BaseButton, {
isFlashy: 'customFlashy',
});
<CustomButton type="button" isPrimary isFlashy>Click me</CustomButton>
// renders <button type="button" className="baseButton primary customFlashy">Click me</button>
// note that `flashy` got removed in favor of `customFlashy`
classBound.withOptions(ClassBoundComponent, oldOptions => newOptions)
Creates a copy of a ClassBoundComponent
by applying the provided function on the existing options and taking the return value of the function as the new options.
const Button = classBound.button('button', 'Button', { variantA: 'variant-a' });
const CustomButton = classBound.withOptions(Button, (options) => ({
className: [options.className, 'fooClass', 'barClass'],
variants: { ...options.variants, variantB: 'variant-b' },
displayName: `Custom(${options.displayName})`,
}));
CustomButton.displayName === 'Custom(Button)';
<CustomButton variantA variantB />;
// renders <button classNames="button fooClass barClass variantA variantB">
Usage with CSS Modules
class-bound-components
is compatible with anything that produces class names as strings. This might be global styles defined in a separate CSS file but also class names that are generated by CSS modules for instance. Instead of the raw class name string you would normally pass to classBound
, simply pass the modularized CSS class name generated by CSS modules.
/* button.css */
.button {
background-color: white;
border: 1px #ccc solid;
}
.isActive {
background-color: #ccc;
}
// button.tsx
import classBound from 'class-bound-components';
import buttonStyles from './button.css';
export const Button = classBound.button(buttonStyles.button, {
isActive: buttonStyles.isActive,
});
// renders <button className="6h3b 0e9c">Click me</button> given that CSS modules
// provides these class names for the module styles
const Container: React.FC = () => <Button isActive>Click me</Button>;
Display Names
Usually, displayName
s in React benefit from the automatic assignment to Function.name
when defining a functional component, which will make the component appear as the name of the function in React DevTools and Error traces.
Unfortunately, this doesn't work for components created with classBound
, since these are defined in a closure. For this, all signatures of classBound
can be provided with an explicit string for the displayName
property of the component.
This can be omitted when using babel-plugin-class-bound-components. This babel plugin tries to infer the displayName
in the fashion like Function.name
would normally do and inlines these into the calls of classBound
, so you don't have to repeat yourself over and over again. Read more in the transformation documentation.
TypeScript
class-bound-components
is built in TypeScript so it supports strong static types out of the box. In particular it is aware of the props that are allowed to be passed to components, be it the passed-down props of the composed element type (e.g., the props of a <button />
element) or props introduced through custom variants. Of course types are also provided for the different signatures of the classBound
function and the member functions on the components.
Ref Forwarding
class-bound-components
handles ref-forwarding automatically. This means for intrinsic elements like div
or img
it will create a React.forwardRef
component as well as in the case of passing it a forwardRef
component as elementType
. For other cases a regular function component is returned.
// Wrapping an intrinsic element
const CustomImage = classBound.img('custom-image');
const imageRef = React.createRef<HTMLImageElement>();
const el1 = <CustomImage ref={imageRef} />; // This works by default!
// Wrapping a ref forwarding component
const RefForwardingImage = React.forwardRef<HTMLImageElement, {}>((_, ref) => (
<img ref={imageRef} />
));
const CustomRefForwardingImage = classBound(
'custom-image',
null,
RefForwardingImage
);
const el2 = <CustomRefForwardingImage ref={imageRef} />; // This works as well!
// Wrapping a non ref forwarding component
const FunctionComponent: React.FC<{}> = () => <img alt="No ref here" />;
const CustomFunctionComponent = classBound(
'custom-image',
null,
FunctionComponent
);
const el3 = <CustomFunctionComponent ref={imageRef} />; // This doesn't work since `FunctionComponent` doesn't have a ref
© 2020 Jannik Portz – License