React components bound to class names. As simple as that.
MIT License
React components bound to class names. As simple as that. Without tagged template literals.
classBound.blockquote('my-blockquote')
extend
, as
, withVariants
and withOptions
Use babel-plugin-class-bound-components
to benefit from:
Proxy
, but still being able to use the classBound[JSX.IntrinsicElement]()
shorthand (e.g., classBound.button('foo')
instead of classBound('foo', null, null, 'button')
)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' });
# 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.
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 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">
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>;
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.
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.
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