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 getModeCn
Since 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.