So far, we've looked at the example of a FormField, which resembles a component since
most FormFields have similar features (some of which we captured in a style object).
However, capturing FormField in a custom component that can support all use-cases is hard, and probably
not a good idea. Since this chapter deals with styling custom components, I will switch to the example
of a DialogButton.
A trim object is a type of style schema that solves two problems:
Here is an example of a trim object for a DialogButton:
type DialogButtonTrimT = {
base: {
componentClassName: string,
root: {
border: any;
color: any;
fontSize: any;
padding: any;
}
Icon: {
margin: any;
size: any;
color: any;
};
},
danger?: ModeOverlayT<FormFieldTrimT>,
disabled?: ModeOverlayT<FormFieldTrimT>
}export const DialogButtonS = {
color: {
disabled: () => 'text-gray-400 hover:text-gray-400 border-gray-400',
blueWithDarkText: () => 'bg-blue-400 text-blue-darkest hover:bg-blue-600',
tealWithDarkText: () => 'bg-teal-400 text-teal-darkest hover:bg-teal-600',
red: () => 'bg-grey-800 text-red',
},
fontSize: {
medium: () => 'text-base',
big: () => 'text-lg',
},
padding: {
medium: () => 'px-4 py-2',
big: () => 'px-6 py-4',
},
Icon: {
margin: {
medium: () => 'mr-2',
none: () => '',
},
},
};export const DefaultDialogButtonTrim = {
base: {
root: {
fontSize: DialogButtonS.fontSize.medium(),
padding: DialogButtonS.padding.medium(),
},
Icon: {
margin: DialogButtonS.Icon.margin.medium(),
// Some style are picked from the IconS style object
size: IconS.size.s20(),
color: IconS.color.gray(),
},
},
danger: { root: { color: DialogButtonS.color.red() } },
disabled: { root: { color: DialogButtonS.color.disabled() } },
};Every trim object has a base key that defines the normal styles that are applied to the component.
In the example, the root element of a DialogButton has a border, a color,
a font size and a padding. We also have an Icon element that has a margin, a size and a color.
As shown in DefaultDialogButtonTrim.ts, you will normally pick these styles from style objects, but you don't have to.
Apart from the base key, the trim object has optional keys that correspond to the different modes of the component.
Since our DialogButton can be in a danger mode or in a disabled mode, we have two optional keys in the trim object.
The style for a disabled DialogButton is created by layering the disabled mode-overlay on top of the base mode
(using the getMode function that we will see shortly).
The componentName in the base mode is a special key that is used to determine the class name
of the root element. This class name can be used in SCSS to add intrinsic styles to the component.
As the next example shows, we can now change the button style by passing in the appropriate trim object:
export type PropsT = {
className?: any;
danger?: boolean;
disabled?: boolean;
iconName: string;
label: string;
trim: DialogButtonTrimT;
// some other props omitted
};
export const DialogButton = (props: PropsT) => {
const { trim, className, danger, disabled, iconName, label, ...rest } = props;
const mode = getMode(trim, { danger, disabled });
return (
<button
className={cn(
trim.base.componentName,
getModeCn(mode.root),
[
'select-none',
'DialogButton',
{
'DialogButton--disabled': disabled,
'DialogButton--danger': danger,
},
],
['flex flex-row items-center', className]
)}
{...rest}
>
<Icon className={getModeCn(mode.Icon)} name={iconName} />
{label}
</button>
);
};export const TealDialogButtonTrim: DialogButtonTrimT = createTrim(
DefaultDialogButtonTrim,
{
base: {
componentName: 'Teal-DialogButton',
root: {
color: DialogButtonS.color.teal(),
},
}
}
);// Note: you should probably apply these special styles using the trim object,
// but this is just an example that shows how SCSS can be used for this purpose.
.Teal-DialogButton {
.DialogButton--danger {
@apply text-3xl;
}
}In this example we see that the DialogButton component takes a trim prop. We can set this property to the
TealDialogButtonTrim object that is defined in trims.ts. Note that TealDialogButtonTrim is based on
DefaultDialogButtonTrim. This shows how you can easily create new trims based on existing ones.
It's a good idea to declare TealDialogButtonTrim to be of type DialogButtonTrimT so that Typescript
immediately catches missing keys in the trim object, rather than reporting errors when you try to use it in DialogButton.
Note that DialogButton still adds the DialogButton class name to the root element, as well DialogButton--danger
and DialogButton--disabled (depending on the mode that the button is in). This allows you to add more intrinsic styles,
as shown in Teal-DialogButton.scss. However, in your code, you could leave out these extra classes.
As was mentioned above, the danger and disabled modes are layered on top of the base mode. We can do this with the
getMode function. In the case of a disabled DialogButton, it executes a mergeDeepRight(trim.base, trim.disabled).
The createTrim function is used to create new trims based on existing trims. It also uses a mergeDeepRight to do this.
In the example, we created TealDialogButtonTrim from DefaultDialogButtonTrim by setting the color in the base mode
to DialogButtonS.color.teal().
Finally, since the contents of a mode are dictionaries, we need a way to convert these to class names. This is done using the
getModeCn function. For example, the result of calling getModeCn(mode.root) is the concatenation of all the values in
the mode.root dictionary.
getMode and getModeCnSince getMode and getModeCn are performed inside the render function, the results of these functions are cached.
The cache is stored in the trim object itself. Please check out the sample code to see how this is done.