Given a list of properties with values (aka Props), and
Given a list of conditions derivated from evaluating properties values (aka variants),
Then compute and output of the properties values that match given conditions (aka Variants that match Props values)
Given a Button component with props
type ButtonProps = {
disabled?: boolean;
color: "blue" | "red";
size: "small" | "large";
loading: boolean;
}
Apply the following classes according to each property value:
color=blue size=small disabled=true
--------------------- | ----------------- | ----------------------------
bg-blue-100 text-blue text-sm py-5 px-2 cursor-not-allowed opacity-5
Regularly one end up writing unmantainable and verbose code, of the sort:
const Button = (props: ButtonProps) => {
const colorStyles = props.color === "blue" ? "bg-blue-100 text-blue" : props.color === "red" ? "bg-red-100 text-red" : "";
const sizeStyles = props.size === "small" ? "text-sm py-5 px-2" : "text-lg py-8 px-4";
const disabledStyles = props.disabled ? "cursor-not-allowed opacity-5" : "";
return <button className={colorStyles + sizeStyles + disabledStyles} ... />
}
Sometimes you do it inline within className={...}
or maybe a bit better by creating yourself a mapping or something,
but the fact this interpolations always end up messy, leaving your code hard to read and mantain later on.
Instead, with this approach you can express those conditions in a declarative way:
const variants: Variants<ButtonProps> = {
"color.blue": "bg-blue-100 text-blue",
"size.small": "text-sm py-5 px-2",
disabled: "cursor-not-allowed opacity-5",
};
const Button = (props: ButtonProps) => {
const [classNames, matches] = vsx(variants, props);
return <button className={classNames} ... />
}
Usage:
<Button color="blue" size="small" disabled /> // className="bg-blue-100 text-blue text-sm py-5 px-2 cursor-not-allowed opacity-5"
const variants: Variants<ButtonProps> = {
"color.red.disabled.false": "bg-red-300 bg-red-500 text-red cursor-pointer"
// can also just compute the condition
"disabled.false.size.large.color.red": undefined,
};
const Button = (props: ButtonProps) => {
const [classNames, matches] = vsx(variants, props);
if (matches.has("disabled.true.size.small.color.red")) {
// this is how to check the criteria was met
// now you can do some custom logic here
}
return <button className={classNames} ... />
}
Usage:
<Button color="red" size="large" disabled={false} /> // className="bg-red-300 bg-red-500 text-red cursor-pointer"
- $nil: right side value is null or undefined
- $all: no conditions always included (can only be included at the root)
- $none -> a valid variant was not found ie:
const variants = {
"color.red": "red",
"color.blue": "blue",
"color.$none:: "magenta" // <-- if props.color is neither "red" or "blue"
}
- the right side value is always thruthy/falsy, objects and functions are casted to boolean as well ie:
const variants = {
"disabled": "disabled", // <-- equivalent to "disabled.true"
}
- when found
props.className
andprops.classNames
are forwarded ie:
const [classNames] = vsx({}, { className: "my-class" }); // classNames="my-class"
// can customised by using the 3rd param -> vsx(variants: Record, props: Record, forwardedKeys: string[]);
const [classNames] = vsx({}, { classes: "class-a class-b" }, ["classes"]); // classNames="class-a class-b"