03. Dynamic "lookup" style objects
š TL;DR
- Weāve extracted styles common to all buttons
- Tailwind Intellisense is working in our
baseClassesvariable - Letās tackle dynamic styles next, starting with the
impactbutton variants!
ā¬ļø Skip to the challenge
š¢ Background
Weāre off to a good start. By extracting styles common to all variants of our Button and customizing the Tailwind Intellisense vscode extension, weāre in a good position to tackle some dynamic style management.
And weāll start with the impact variants.
Three impact levels
The button source code tells us that a button can have three impact values: bold, light, and none.
We have defined a TypeScript type for these impact values:
type ButtonProps = {
// ...
impact: 'bold' | 'light' | 'none'
}
If youāre not familiar with TypeScript, donāt worry - you wonāt have to write any TypeScript yourself. This is here to help us construct the correct dynamic styles, as weāll see in a second.
Hereās what these three variants look like:
Go on, click on these buttons. Itās pretty satisfying š¤
Building a styles ālookupā directory
We want to apply different Tailwind classes to the Button based on the value of the impact prop.
Remember, we need to make sure we use entire class strings so that the JIT engine finds and generates those classes in our CSS output.
The best way to make sure all the Tailwind classes we need are present as full strings? Creating an āinventoryā or directory of all the possible class permutaitons.
A collection of strings grouping the classses that should be applied for each possible variant permutation.
In the case of the impact prop, we have three possible values - so letās create an impactClasses lookup directory.
This will be an object with a key for each possible prop value:
const impactClasses = {
bold: '...',
light: '...',
none: '...',
}
Applying dynamic styles
Since the keys of our impactClasses lookup object are mirroring the possible values of the impact prop, we can dynamically reach for the correct key by using the prop value passed to a given Button.
For example, take the following code:
<Button impact="light">Light button</Button>
In that scenario, we want to apply the string of classes located in impactClasses.light.
We can do exactly that by using the bracket syntax:
<button className={impactClasses[props.impact]} />
Great!
But this will only work if the keys inside the impactClasses object perfectly mirror the possible values for the impact prop.
And this is why weāre using TypeScript.
TypeScript Lite to the rescue
In the challenge code, youāll notice that weāve annotated the impactClasses object with a TypeScript Record:
const impactClasses: Record<ButtonProps['impact'], string> = {}
Adding that Record sets expectations on this impactClasses object. It should have a key for each possible impact variant, and the value for that key should be a string.
In other words, it ensure that the impactClasses object mirrors the shape of the possible impact variants defined in ButtonProps.
Like Matt Pocock often says, TypeScript is like your teacher looking over your shoulder, and making sure you correctly cater for each variant, and donāt accidentally add any other key to the impactClasses object.
That teacher will keep ābotheringā you until youāve defined the correct impactClasses keys.
It just wants you to succeed!
Merging multiple class lists together
Take the code snippet from before, where we reach for the correct impact Tailwind classes:
<button className={impactClasses[props.impact]} />
With that code, we have thrown away our previous baseClasses containing the common styles for all buttons.
We actually need to merge both the baseClasses and impactClasses in the className attribute!
We could construct a template string that merges them. But Iāve added a neat little helper cx() function in the project:
function cx(...classes) {
return classes.filter(Boolean).join(' ')
}
This function lets you pass a comma separated list of multiple class strings, and it will take care of merging them them.
Itās just a neat little syntactic helper, to mimic some of the functionality of a library like classnames.
With that function, we can combine our classes like so:
<button className={cx(baseClasses, impactClasses[props.impact])} />
As we compose more and more class lists, this syntax is arguably a little easier to read than a long template string!
š Your challenge š
In the Gitpod workspace below:
-
In
src/components/button.tsx, create a key for eachimpactprop inside theimpactClasseslookup object. TypeScript will guide you in this task! -
For each key, create a string of the Tailwind classes used for that specific variant, based on what you see in
src/partials/hardcoded-buttons.astro. -
In
button.tsx, merge the correct, dynamicimpactClassesin the buttonāsclassNameattribute.
āļø Good luck!
Open in Gitpod ā