03. Dynamic "lookup" style objects
š TL;DR
- Weāve extracted styles common to all buttons
- Tailwind Intellisense is working in our
baseClasses
variable - Letās tackle dynamic styles next, starting with the
impact
button 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 eachimpact
prop inside theimpactClasses
lookup 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, dynamicimpactClasses
in the buttonāsclassName
attribute.
āļø Good luck!
Open in Gitpod ā