mrtamagotchi / twiv

Tailwind inline variants. A styling util (with type inference) that keeps your styles close to the markup. Like CVA without steriods.
MIT License
1 stars 0 forks source link

Apply classes based on combined props state #2

Open mrtamagotchi opened 4 months ago

mrtamagotchi commented 4 months ago

Background

Currently, Twiv can only apply classes matching one state. If we would like to add a class if propA === X && propB === Y we have to do some JS gymnastics.

Possible solution

Only half-baked ideas here, but some operator syntax could be interesting:

interface MyComponentProps {
    direction: "left" | "right"; 
    spacing: "small" | "large";
}

function MyComponent({direction, spacing}: MyComponentProps) {
    const twiv = useTwiv([direction, spacing])

    // There are better solutions for this dumb example, but you get the idea
    return (
        <div 
            classNames={twiv({
                small: "text-sm",
                large: "text-lg",
                left: "text-left",
                right: "text-right",
                "left && small": "pl-1",
                "left && large": "pl-4",
                "right && small": "pr-1",
                "right && large": "pl-4"                
            })}
        />
    )
}

A question of operators

If this is a good feature to add, there are three main questions that need to be addressed:

Uncertainties

mrtamagotchi commented 3 months ago

After a lot of thinking and som AFK discussions with other devs I've come to a couple of conclusions:

Findings

Logical NAND is complicated and dumb

... and you should probably simplify your component if you need it. This was a clear case of "Your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should" - Jeff Goldblum, Jurassic Park, 1995.

Multiple operators in one compound state is overkill

Twiv should be simple and cover most common cases. Thinking back on all my years as a dev, the most common cases of styling on multiple states has been either stateA && stateB or stateA || stateB. Once again, if more complex styling is needed, Twiv should not be the solution.

Using operators between all states is redundant

Tying into the point above, stateA && stateB && stateC becomes redundant in two ways:

Solution

Given the findings above I've merged in a solution I decided to call Opcodes. The syntax follows the pattern ALL: <list of variant state names separated by a space>. I've chosen to have it in caps as it (somewhat creatively) follows the Tailwind config syntax of "DEFAULT:" (if you squint and pretend that DEFAULT: is considered to be some kind of "meta-value").

Currently, two opcodes exist:

This removes the need for typing out multiple operators, makes evaluation a lot more performant and gives you a pretty sweet and concise syntax. It's also easily extandable if the need for other evaluations would arise.

With this, the example in the first post in this thread, it would look like this:

twiv({
    small: "text-sm",
    large: "text-lg",
    left: "text-left",
    right: "text-right",
    "ALL: left small": "pl-1",
    "ALL: left large": "pl-4",
    "ALL: right small": "pr-1",
    "ALL: right large": "pl-4"                
})

Issues with this solution

The biggest downside of this solution is that TS support becomes a bit shady. Ideally, you would want a type that checks for a string that follows the pattern: One TwivOpcode, n amount of ValidVariantName, all separated by spaces. This, unfortunately, makes TS shit its pants for components with a reasonable amount of states as such a type quickly outgrows the max of 100_000 variations.

Conclusion

I've decided to go for this solution for now, just to see how it feels in a real project. The Opcodes feature is marked as experimental, until there is a final verdict of its ergonomics, performance and usefulness.