Skip to content

Design constraints with css classes and typescript validation and auto-completion #87

Description

@kaaboaye
  • Your full name: Mieszko Wawrzyniak
  • Target audience (beginner/intermediate/advanced/everyone): intermediate
  • Estimated duration: 20 min
  • Keywords:

<Title of your talk>

Design constraints with css classes and typescript validation and auto-completion

Title is WIP

The talk

One-sentence summary

I'll explain how you can leverage Typescript template literal types to create both run-time and compile time classes enabling your very own Tailwind-like developer experience.

What's the format — is it a case study, a live coding session, a workshop or something else?

case study

Tell us more about the talk

I'll most likely go through the following file and show step by step how Typescript's type system can infer types in various real time scenarios.

// register global stylesdeclare global {interfaceWindow{sfrStyles?: HTMLStyleElement;}}constatomicClasses=(()=>{constbaseClasses={hide: "display: none",block: "display: block","inline-block": "display: inline-block",flex: "display: flex","inline-flex": "display: inline-flex","flex-wrap": "flex-wrap: wrap",flex1: "flex: 1",flex2: "flex: 2",flex3: "flex: 3",flex4: "flex: 4",flex5: "flex: 5",flex6: "flex: 6",capitalize: "text-transform: capitalize",lowercase: "text-transform: lowercase",uppercase: "text-transform: uppercase","margin-0auto": "margin: 0 auto","margin-left-auto": "margin-left: auto","position-relative": "position: relative","background-color-initial": "background-color: initial","background-color-white": "background-color: white","user-select-none": "user-select: none","cursor-pointer": "cursor: pointer","color-white": "color: white","max-width400": "max-width: 400px","max-width720": "max-width: 720px","pointer-events-auto": "pointer-events: auto","white-space-pre-wrap": "white-space: pre-wrap",}asconst;functioncomputeVariations<T1extendsstring,T2extendsstring,V1extendsstring,V2extendsstring>([names,subNames]: readonly[readonlyT1[],readonlyT2[]],values: readonly(readonly[V1,V2])[]){returnnames.flatMap((name)=>[
...values.map(([valueName,value])=>[`${name}${valueName}`,`${name}: ${value}`]asconst),
...subNames.flatMap((subName)=>values.map(([valueName,value])=>[`${name}-${subName}${valueName}`,`${name}-${subName}: ${value}`,]asconst)),]asconst);}constsizes=[["4","4px"],["6","6px"],["8","8px"],["12","12px"],["16","16px"],["18","18px"],["24","24px"],["32","32px"],["48","48px"],["64","64px"],]asconst;constcolors=(["blue","green","red","gray"]asconst).flatMap((color)=>(["1","2","3","4","5","6","7","8","9","10"]asconst).flatMap((step)=>[`${color}-${step}`]asconst));constcolorOptions=([["background","background-color"],["color","color"],]asconst).flatMap(([name,property])=>colors.map((color)=>[`${name}-${color}`,`${property}: var(--${color})`]asconst));constsizeEntries=computeVariations([["width","max-width","min-width","height","max-height","min-height"],[],],[
...sizes,["128","128px"],["100","100%"],["-min","min-content"],["-max","max-content"],]);constspacingEntries=computeVariations([["margin","padding"],["top","bottom","left","right"],],[["0","0"], ...sizes]);constgapEntries=computeVariations([["gap"],[]],[...sizes,["-items","var(--gap-items)"],["-cards","var(--gap-cards)"]]);constsizedEntries=computeVariations([["border-radius"],[]],[["6","6px"],["16","16px"],["100","100%"],]);constborderEntries=computeVariations([["border"],["right"]],[["0","0"],["-none","none"],]);constflexDirectionEntries=computeVariations([["flex-direction"],[]],[["-row","row"],["-column","column"],["-row-reverse","row-reverse"],["-column-reverse","column-reverse"],]);constcontentEntries=computeVariations([["justify-content"],[]],[["-space-between","space-between"],["-space-around","space-around"],["-center","center"],["-end","end"],["-flex-end","flex-end"],]);constitemsEntries=computeVariations([["align-items","align-self","justify-items"],[]],[["-start","start"],["-center","center"],["-stretch","stretch"],["-end","end"],["-flex-end","flex-end"],]);consttextAlignEntries=computeVariations([["text-align"],[]],[["-left","left"],["-center","center"],["-right","right"],]);constoverflowEntries=computeVariations([["overflow"],["x","y"]],[["-visible","visible"],["-hidden","hidden"],["-auto","auto"],]);constfontSizeEntries=computeVariations([["font-size"],[]],sizes);typeNameFromEntries<Textendsreadonly(readonly[string,string])[]>=T[number][0];constexternalClasses=["btn-hidable","btn-icon-right","pagination-hide-steps",]asconst;typeName=|keyoftypeofbaseClasses|NameFromEntries<typeofsizeEntries>|NameFromEntries<typeofcolorOptions>|NameFromEntries<typeofspacingEntries>|NameFromEntries<typeofgapEntries>|NameFromEntries<typeofsizedEntries>|NameFromEntries<typeofflexDirectionEntries>|NameFromEntries<typeofcontentEntries>|NameFromEntries<typeofitemsEntries>|NameFromEntries<typeoftextAlignEntries>|NameFromEntries<typeofoverflowEntries>|NameFromEntries<typeofborderEntries>|NameFromEntries<typeoffontSizeEntries>|typeofexternalClasses[number];constclasses=Object.freeze(Object.assign(Object.create(null)asobject,baseClasses,Object.fromEntries(sizeEntries),Object.fromEntries(colorOptions),Object.fromEntries(spacingEntries),Object.fromEntries(gapEntries),Object.fromEntries(sizedEntries),Object.fromEntries(flexDirectionEntries),Object.fromEntries(contentEntries),Object.fromEntries(itemsEntries),Object.fromEntries(textAlignEntries),Object.fromEntries(overflowEntries),Object.fromEntries(borderEntries),Object.fromEntries(fontSizeEntries),Object.fromEntries(externalClasses.map((name)=>[name,""]asconst)))as{[keyinName]: string});// register stylesconststyleContent=Object.entries(classes).filter(([,style])=>style).map(([className,style])=>`.sfr-${className} {${style} !important;}`).join("\n");consttextNode=document.createTextNode(styleContent);if(window.sfrStyles){window.sfrStyles.firstChild?.remove();window.sfrStyles.appendChild(textNode);}else{window.sfrStyles=document.createElement("style");window.sfrStyles.appendChild(textNode);document.head.appendChild(window.sfrStyles);}returnclasses;})();typeName=keyoftypeofatomicClasses;// let's use arguments directly to prevent webpack from converting it to array/* eslint-disable prefer-rest-params */exportconstcssCommons=functioncssCommons(): string{constparts: string[]=newArray(arguments.length*2);letpartsIdx=0;// those loops aren't recommended because of readability but they are still the fastest way// to iterate over an array// eslint-disable-next-line no-plusplusfor(letargumentsIdx=0;argumentsIdx<arguments.length;argumentsIdx++){// since app wont crash when unknown css classes are used we don't need this check// in production builds and webpack will remove this code when bundlingif(process.env.NODE_ENV!=="production"){if(!((arguments[argumentsIdx]asName)inatomicClasses)){thrownewError(`Unknown atomic class. "${arguments[argumentsIdx]asName}" is not available.`);}}// parts.push() is a no-go since we preallocate the array/* eslint-disable no-plusplus */parts[partsIdx++]=" sfr-";parts[partsIdx++]=arguments[argumentsIdx];/* eslint-enable no-plusplus */}// apply is faster then "".concat(...parts)returnString.prototype.concat.apply("",parts);}as(...classNames: readonly[Name, ...(readonlyName[])])=>string;/* eslint-enable prefer-rest-params */

And explain how we've got from the code above the following results

image

image

You

Co-Funder and CTO at Surfer Local. https://surferlocal.com

A few words about yourself

https://www.linkedin.com/in/mieszko-wawrzyniak/

I'll work up something later if my talk is accepted.

How can we find you on social media?

It's best to email me.

Would you be willing to have a Q/A session after the talk?

Sure

Do you mind if we record the event?

Nope, go ahead

Is there anything we can help you with?

Nope

I was invited to post my proposal by @hasparus

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions