Brainpower ++ met Senior React Developer Demiën
Design systems voor front-enders: hoe je een component library bouwt die overal werkt.
Een design system helpt bij het samenbrengen van verschillende disciplines en zorgt voor verbeterde cohesie en consistentie tussen verschillende uitingen en producten van een organisatie. Daarnaast draagt een uitgewerkt design system bij aan de efficiëntie waarmee een front-end kan worden gebouwd. Ook verbeterd het de kwaliteit van interfaces wanneer deze gebouwd worden door developers die minder thuis zijn in het frontend landschap.
Een design system is niet hetzelfde als een merk of huisstijl. Je zou een design system kunnen beschouwen als een product dat volgt uit een huisstijl; het is dus meer dan een logo, lettertype en kleurstelling. Het bevat bijvoorbeeld ‘assets’, documentatie en processen die beschrijven hoe digitale uitingen moeten worden vormgegeven.
Deze guide zal je stap voor stap begeleiden bij het implementeren van een design system in je front-end. We zullen bespreken wat een design system is, waarom het belangrijk is en hoe het kan bijdragen aan de cohesie en consistentie van je digitale uitingen. We zullen ook de principes van Atomic Design introduceren en laten zien hoe je een component library kunt bouwen die het design system implementeert met behulp van Stencil.JS, Storybook en Docusaurus. We zullen dieper ingaan op elk van deze onderwerpen en je stap voor stap begeleiden bij het opzetten van je eigen front-end design system en component library.
Wat kun je verwachten?
Deze guide zal je stap voor stap begeleiden bij het opzetten van een component library als product van het design system. We zullen bespreken wat een design system is, waarom het belangrijk is en hoe het kan bijdragen aan de cohesie en consistentie van je digitale uitingen. We zullen ook de principes van Atomic Design introduceren en laten zien hoe je een design system kunt bouwen met behulp van Stencil.JS, Storybook en Docusaurus. We zullen dieper ingaan op elk van deze onderwerpen en je stap voor stap begeleiden bij het opzetten van je eigen front-end design system en component library.
Disclaimer: Design System !== Component Library.
Wat is dit niet? Deze guide gaat niet in op het proces van het maken van een design system, maar richt zich op het begrijpen en vertalen van een design system naar een product dat door jou en andere developers gebruikt kan worden. Het doel is om je inzicht te geven in de principes en concepten achter een design system, zodat je beter in staat bent om het toe te passen in je eigen projecten. Daarnaast duiken we in tips, uitleg en voorbeelden om je te helpen bij het implementeren van een design system in je softwaretoepassingen.
Atomic Design.
Atomic Design (Brad Frost, 2016) beschrijft een ontwerpmethode aan de hand van scheikunde, waar gesproken wordt in atomen, moleculen en uiteindelijk organismen. Frost deelt zijn methode op in vijf individuele stappen die uiteindelijk samen een hiërarchisch design system vormen. Deze vijf stappen, van Atomic Design, zijn:
#1 Atomen.
In de scheikunde zijn atomen de bouwstenen van alle materie, dan geldt in Atomic Design dat atomen de bouwstenen zijn waaruit alle gebruikersinterfaces bestaan. Neem bijvoorbeeld labels, inputs, buttons, paragraven, headers en alle andere elementen die niet verder opgesplitst kunnen worden zonder dat de functionaliteit verdwijnt.
Elk atoom heeft zijn eigen unieke eigenschappen, zo bevat bijvoorbeeld waterstof één electron en helium twee. Hier zijn die eigenschappen nu net wat anders, denk aan stijlen zoals lettergrootte, breedte, hoogte en kleur.
#2 Moleculen.
Een groep atomen die aan elkaar verbonden zijn, worden in de scheikunde een molecuul genoemd. Een water molecuul bestaat zo uit twee waterstof atomen en één zuurstof atoom. Hier zouden we kunnen stellen dat een een ‘form-entry’-molecuul bestaat uit een ‘label’-atoom en een ‘input’-atoom.
In interfaces is een molecuul een relatief simpele groep van UI elementen (atomen) die samen eenzelfde doel willen vervullen. Door deze elementen (atomen) later te ‘verbinden’, waarborg je het ‘single responsibility principe’. Waardoor je zorgt dat elk atoom één zo specifiek mogelijke taak heeft, en die goed vervuld.
#3 Organismen.
Bij organismen spreken we over een vrij complexe groep UI Componenten (moleculen). Deze moleculen leven naast elkaar en vervullen samen een bredere functie, bijvoorbeeld: een navigatiebalk, een header, een artikel. Organismen zijn dus een verzameling van verschillende, of dezelfde, moleculen die een grotere rol vervullen en echt body aan een interface geven.
#4 Templates.
Vanaf hier neemt Frost afscheid van het scheikundige voorbeeld om plaats te maken voor de ons wel bekende jargon. Templates beschrijven een structuur die componenten (organismen) in een structuur plaatst. Templates helpen dus bij het uniform organiseren van content.
#5 Pagina's.
Pagina’s, het definitieve eindproduct. Een pagina is in feite een template, waarvan de vakjes gevuld zijn met organismen.
Big Bang Theory, what became before atoms.
Nu we de ‘Atomic Design’ methode beter begrijpen kunnen we terugwerken naar een design system. Wat ging er vooraf aan atomen? Hoe atomen er exact uitzien is afhankelijk van de ontwerpkeuzen die gemaakt zijn door de designer, simpelweg een ‘ontwerp’. Maar deze ontwerp eigenschappen worden vandaag de dag steeds beter onderzocht, getest, gepland, en uiteindelijk vastgelegd in design tokens. Maar wat zijn dat precies, design tokens?
Om een les uit het boek van Frost te nemen, zouden we de design tokens het best kunnen beschrijven als protonen, neutronen en electronen: het zijn de bouwstenen van een atoom. In het kort zijn design tokens - of tokens - stukjes data die een kleine, repeterende ontwerpkeuze representeren. Denk hier aan lettergrootte, breedte of kleur (de eigenschappen van een atoom). Door deze waarden los te trekken van specifieke componenten, is het mogelijk om een wendbaar, platform-agnostisch design system te maken.
In essentie wil een design token zowel generiek als uniek zijn, zodat een grote wijziging plots heel eenvoudig wordt, maar ook zodat een kleine wijziging klein blijft. Een token zorgt hiervoor door zich op te splitsen in globale-, semantische- en component-tokens. Laten we daar dieper op ingaan.
Globale tokens.
Deze tokens bevatten basiselementen zoals kleuren, typografie en afmetingen die op een globaal niveau worden toegepast in het gehele design system. Door deze eigenschappen te vereenvoudigen tot globale tokens, wordt het mogelijk om op eenvoudige wijze wijzigingen door te voeren die een groot effect hebben op het gehele ontwerp. Bijvoorbeeld, als je de primaire kleur van een ontwerp wilt wijzigen, is het voldoende om het globale token aan te passen, wat automatisch de kleur in alle componenten en elementen binnen het systeem verandert.
Voorbeelden van globale tokens zijn:
blue-900: #0265DC, border-radius-lg: 25px, font-body: 'Arial'
Semantische tokens.
Semantische tokens (ook wel aliassen) leggen de nadruk op de betekenis en context van globale tokens. Ze zijn gerelateerd aan specifieke eigenschappen die betrekking hebben op de functie of rol van een element binnen de gebruikersinterface. Hierbij valt te denken aan tokens die de stijl van knoppen, formulier elementen of kopteksten definiëren. Het gebruik van semantische tokens zorgt ervoor dat ontwerpers en ontwikkelaars gemakkelijk de juiste stijlkenmerken kunnen toepassen op specifieke elementen, waardoor een consistente en begrijpelijke gebruikerservaring ontstaat.
Voorbeelden van semantische tokens zijn:
color-primary: blue-900, surface-background: white-900, on-surface-background: black-600
Component tokens.
Component tokens zijn de laatste tussenstap en beschrijven de waarden die specifiek betrekking hebben op het component in kwestie. In principe refereren deze tokens aan de semantische tokens, maar zorgen dus voor een hoge cohesie maar een lage koppeling met de rest van het design system, waardoor ze eenvoudig te wisselen zijn.
Voorbeelden van component tokens zijn:
card-background: surface-background, button-primary: color-primary
Maar ik ben geen designer, wat moet ik hiermee?
Net hebben we in sneltreinvaart geleerd hoe een design system zo ongeveer werkt, en dit is vaak ook hoe ver dit geïmplementeerd wordt door een developer. Maar als front-end developer is het aan ons om dit design system zo goed mogelijk te implementeren in de uiteindelijke softwareproducten. Stel je voor, je ontvangt een nieuw design system en jouw organisatie heeft twee, drie of meer applicaties. Één in React, één in Angular, en één zonder front-end framework - maar het nieuwe design system moet worden geïmplementeerd. Dat betekend dat drie teams nu verschillende componenten moeten gaan maken op basis van het design system. Gaat dat lukken? Waarschijnlijk wel. Wordt alles 100% consistent? Waarschijnlijk niet. Kost het veel tijd? Waarschijnlijk wel. Hoe kan dit worden gestroomlijnd?
Component libraries.
Wanneer je organisatie een design system, of huisstijl, hanteert en verschillende digitale uitingen heeft is het het overwegen waard om een component library te ontwikkelen. Zo bouw je een single source of truth die er in ieder project hetzelfde uitziet en hetzelfde werkt. Handig, want zo houd je de betrouwbaarheid en professionaliteit van de digitale uitingen van jouw organisatie op niveau.
Een internere component library kun je beschouwen als een eigen shadcn-ui, material-ui of bootstrap. Alle componenten worden één keer gebouwd en gestijlt, precies zoals de ontwerper dat voor ogen heeft, vervolgens kunnen developers in andere producten componenten kiezen uit deze collectie. Hierdoor bespaar je tijdens de initiële ontwikkeling van je nieuwe product al erg veel tijd en voorkom je dubbel werk is verschillende code bases.
De grootste winst is te behalen wanneer er een wijziging plaatsvind in het design system. De componenten in de component library krijgen nieuwe stijlen en vervolgens hoeven de deelprojecten slechts nog hun packages te updaten. Was er geen component library, dan had het waarschijnlijk maanden of jaren geduurd voordat alle producten, op alle plekken, de nieuwe stijlen hadden gekregen.
Framework agnostisch.
Omdat je niet altijd zeker weet welke stack het volgende project gebruikt, is het handig om platform-agnostisch te werken en dicht bij de basis van het web te blijven. Daarnaast moet het eenvoudig zijn en geen grote leercurve voor je collega’s zijn. En efficiënt, future proof en bovenal snel.
In de basis zijn web components een mooie tool om hiermee van start te gaan, web components werken namelijk native in de browser dus ook in projecten die gebruik maken van andere frameworks. In de basis zijn web components (lees meer) een mooie tool om hiermee van start te gaan, web components werken namelijk native in de browser dus ook in projecten die gebruik maken van andere frameworks. Net als veel front-end frameworks, maken web components gebruik van een shadow dom, zo wordt de nieuwe render voorbereid voordat het daadwerkelijk gerenderd word; dat houdt een update in de runtime snel en efficiënt. Het grote verschil is dat een web component dit native uit kan voeren, zonder afhankelijk te zijn van een groot framework. Wil je weten hoe je web components bouwt? Bekijk dan de zesdelige blog van Wessel Loth over web components.
Stencil.JS is een library die het mogelijk maakt om een schaalbare component library te maken zonder alles als native web components te bouwen. Het voordeel is dat Stencil wat handige hulpmiddelen bevat om sneller, duidelijker en efficiënter web components te ontwikkelen die je kan exporteren in kleine bundles. De output van Stencil bevat geen overhead zoals een framework, maar enkel barebone web components die jij hebt geschreven. Ook zorgt Stencil voor polyfills om browser support te garanderen, ook wanneer deze misschien nog geen native support hebben voor web components. In de nieuwste versie van Stencil is er support voor alle browser(s/updates) sinds grofweg 2020, dat is ruim 97% van alle gebruikers wereldwijd. Kies je voor een eerdere versie van Stencil, dan heb je zelfs support in Internet Explorer en Pre-Chromium Edge (voor 2020).
Server side rendering.
In principe ondersteunt Stencil server side rendering (SSR) door een extra "hydration" laag toe te voegen. Deze aanpak zorgt ervoor dat de inhoud van de componenten vooraf wordt weergegeven aan de gebruiker, zelfs voordat JavaScript is geladen en uitgevoerd. Wat erg goed is voor SEO doeleinden.
Echter, in de nieuwste versie van Next.js (Next.js 11) werkt SSR met Stencil op dit moment nog niet goed. Het kan problemen veroorzaken in combinatie met de nieuwe Next.js app router. Als je SSR wilt gebruiken met Stencil in Next.js, wordt aanbevolen om een oudere versie van Next.js te gebruiken die de oudere page router gebruikt. In die versie werkt SSR met Stencil zonder problemen.
Voor meer informatie over het gebruik van SSR met Stencil in Next.js, kun je de documentatie van Next.js raadplegen en controleren op updates over de ondersteuning van Stencil.
Bruikbaar.
Wanneer je wil dat je product - dus ook een component library - gebruikt wordt, is het belangrijk dat je het ook bruikbaar maakt. Zorg dus dat er documentatie is die duidelijk is door het gebruik van interactieve voorbeelden, code, beschrijvingen van properties, methoden en events: zoals je het gewend bent van de component libraries die je misschien al graag gebruikt. Mijn keuze is hierom gevallen op Docusaurus, het wordt al gebruikt door honderden vooraanstaande libraries (Jest, Redis, Ionic, React Native en meer) en is daardoor erg herkenbaar in gebruik en bewezen effectief. Bijkomend voordeel is dat alles met Markdown is te schrijven, en geserveerd kan worden als een static site. Ideaal voor bijvoorbeeld GitHub.
Om het product uiteindelijk ook echt in handen van de developers te krijgen, kun je het publiceren als een NPM package. Dit kan op verschillende manieren, publiekelijk via de NPM registry of tegen forse kosten privé. Maakt je organisatie gebruik van Azure DevOps, dan kun je het NPM package publikelijk of privé publiceren als Artifact. Gebruik je GitLab of GitHub, dan ook heb je die mogelijkheid.
Dat is, in vogelvlucht, de basis van wat wij gaan ontwikkelen. In het volgende hoofdstuk neem ik je mee hoe ik dit in de praktijk breng.
Bouwen van een component library.
Voor deze component library maken we gebruik van drie libraries. Allereerst StencilJS, dit is de basis van het project en zorgt uiteindelijk dat we herbruikbare componenten hebben. Als hulpmiddel voor de ontwikkeling maak ik gebruik van Storybook, dat is een tool die je componenten in een test-omgeving weergeeft en het makkelijk maakt verschillende properties aan te passen. Ten slotte gebruik ik Docusaurus om een statische documentatie te maken voor de component library zodat andere developers de componenten kunnen gebruiken, zonder het project lokaal te hoeven draaien.
Deze drie libraries leven in één repository: een soort “monorepo-light”. Laten we die gaan opzetten. Navigeer naar je projectmap of maak een nieuwe aan, ik noem de mijne component-library-demo. Navigeer in je terminal, of command prompt, naar deze map voor de volgende stappen.
Monorepo: Lerna.
Zorg allereerst dat Lerna globaal is geïnstalleerd, deze tool gebruiken we voor het beheren van de monorepo: npm install --global lerna. In de projectmap, component-library-demo/, voer je vervolgens deze commando’s uit:
Stencil.JS.
Tijd om Stencil toe te voegen, navigeer in je terminal naar de map ./packages/, en voer het commando npm init stencil uit. In het prompt kies je voor component, omdat we een component library maken en geen app of progressive web app. Mijn project-name is stencil-library, voor nu voldoende. Deze kunnen we later aanpassen.
Navigeer nu naar je project, cd stencil-library en voer npm i uit om alle resources toe te voegen. Nu is Stencil geïnstalleerd en kunnen we Storybook toevoegen.
Storybook.
Nog steeds in je Stencil map, stencil-library, voer je het commando npx sb init --type html uit.
Dit commando installeert via NPX, storybook met het projecttype HTML. Je wordt gevraagd welke compiler je gebruikt, dit heeft geen verdere invloed op de rest van je project omdat we storybook niet publiceren. Ik koos voor Vite omdat dat mijn voorkeur heeft, mocht ik dat later toch willen.
Nu moet Stencil geïntegreerd worden met Storybook. Als Storybook automatisch is gestart, kun je het proces stoppen door de knoppen ctrl + c in de terminal in te drukken.
Open de Stencil map in je code editor en je zal zien dat er nu een .storybook map is toegevoegd. Open het bestand .storybook/preview.js, hierin moeten we de Stencil resources instantiëren. Voeg het volgende snippet toe aan de bovenkant van het bestand, de rest kun je laten staan.
Omdat de loader alleen geüpdate wordt als je stencil bouwt, moet je het commando npm build uitvoeren om wijzigingen te zien in Storybook.
Docusaurus.
Als laatste voegen we Docusaurus toe om een plekje te maken voor onze documentatie. Hiervoor gaan we weer terug naar de root-map in onze terminal, ./. Voer nu het commando npx create-docusaurus om Docusaurus te installeren, als naam kies ik wederom voor een duidelijke naam die het doel van de map aangeeft in de monorepo: docs.
Ik wil alleen documentatie weergeven in mijn docs, geen blogs of landingspagina: omdat het slechts dient voor intern gebruik. Daarom verwijder ik de mappen ./docs/pages en ./docs/blogs. Vervolgens moet er wat configuratie gedaan worden, open daarvoor ./docs/docusaurus.config.js:
Nu de blogs en landing pages verwijderd zijn, moet de homepagina nog aangepast worden. Open hiervoor ./docs/docs/intro.md, en voeg tussen de header slug: '/' toe om te zorgen dat dat de root-pagina is.
Ook hebben we een loader bestand toegevoegd aan de clientmodules dat nog niet bestaat. Maak deze aan in ./docs/component-library-loader.js met de volgende inhoud:
Dit bestand laadt de component library in en voegt polyfills to voor een bredere browserondersteuning. Nu halen we de library nog uit de onze lokale code, later passen we dit aan naar het NPM package dat we gaan publiceren.
Framework integration.
Voor optimale support in andere frameworks, zoals React, Angular en Vue kunnen we nog wat extra configuratie uitvoeren. Hoewel componenten technisch gezien al moeten werken, zijn (vooral typescript georienteerde) frameworks niet altijd even welwillend in de ondersteuning van Web Components.
Stencil.JS heeft guides over hoe je dit toepast voor verschillende target frameworks. Ik behandel hier React, de andere frameworks hebben een bijna identieke integratie. https://stenciljs.com/docs/overview
React.
Navigeer in een terminal naar de root van je project. Maak hier een tsconfig.json aan met de volgende inhoud:
Vanuit daar gaan we een nieuw package aanmaken die dient als de export voor je React specifieke componenten.
Maak nu ook in ./packages/react-library een tsconfig.json aan met de volgende inhoud:
Pas vervolgens de package JSON aan volgens dit schema
Vervolgens gaan we terug naar je hoofdproject, ./packages/stencil-library, en voegen we het volgende NPM package toe: npm install @stencil/react-output-target --save-dev. In de stencil.config.ts voegen we de volgende regels toe:
Waar
Hernoem nu ./packages/react-library/lib/react-library.js naar ./packages/react-library/lib/index.ts en vervang de inhoud met:
Nu voeg je je main-library toe aan je package.json, zodat er een relatie is. En pas je de package name aan:
Ook Angular, Ember of Vue ondersteunen? Volg dan de guides op: https://stenciljs.com/docs/overview
Nu alle libraries toegevoegd zijn, kunnen we starten met het vastleggen van de design tokens.
Design tokens.
De volgende stap is het vastleggen van de kleinste bouwstenen: de design tokens. In het bestand ./packages/stencil-library/src/global-design-tokens.json maak ik een logisch bestand dat is opgebouwd om de visie van Salesforce te volgen. SalesForce wordt veelal beschouwd als de uitvinder van Design Tokens, of dat ook echt waar is kun je je afvragen. In ieder geval zijn zij een van de eersten die een standaard publiceerden.
Global tokens.
In dit JSON bestand definiëren we de global tokens. Dat zijn de tokens die geen doel op zich hebben voor gebruik, ontwerp of intentie. Straks, als CSS variabel, wil ik dat die als volgt zijn opgebouwd:
--
Namespace is bijvoorbeeld de naam van je project, bedrijf of doel. Ik gebruik in dit voorbeeld arcady. Het niveau gaat om de laag van het token, dus globaal, semantisch of component. In dit geval global, dus ik kies g. Categorie is het type variabel, bijvoorbeeld kleur, dus ik kies voor color. Ten slotte property, bij mij nu de naamgeving van de kleur. Dit kan simpelweg green zijn, maar ook een tint als green-500.
Als we dat samenvoegen, ziet mijn variabel er zo uit: --arcady-g-color-green-500
Omdat ik het bestand opbouw in JSON format, wordt dat het volgende:
Om mijn semantische variabelen te kunnen definiëren, heb ik eerst de globale waarden nodig. Die vormen immers de input voor de semantische tokens. Dus ik neem je eerst even mee op een uitstapje hoe ik die gedefineerd krijg.
In het configbestand van Stencil, ./packages/stencil-library/stencil.config.ts, kun je een property toevoegen globalStyle, de waarde hiervan is het relatieve pad naar een global css bestand. Omdat ik het voor een component library good practice vind om globale stijlen te meiden, gebruik ik het slechts om de global variables in vast te leggen. Daarom maak ik een bestand ./packages/stencil-library/src/globals/design-tokens.scss, hierin leg ik de referenties vast naar de global tokens - die we straks gaan genereren - en de semantische tokens die we handmatig gaan vullen. De component tokens leg ik vast in de stijlen van elk individuele component dat we gaan bouwen.
Ook maak ik vast het bestand ./packages/stencil-library/src/globals/semantic-tokens.scss aan met als inhoud slechts een placeholder:
Nu kunnen we ook dit bestand gaan refereren in de config van stencil:
En ten slotte moeten deze globale stijlen nog aan Storybook worden toegevoegd. In de projectmap ./packages/stencil-library/, voer het commando npm install --save-dev sass uit om Sass ondersteuning toe te voegen aan Storybook. Voeg de regel import '../src/global/design-tokens.scss'; toe, bovenin .storybook/preview.js. De map src/stories kan nu verwijderd worden.
Omdat we de vastgelegde tokens willen converteren naar CSS variables maak ik een util. In ./packages/stencil-library/src/utils/create-global-variables.ts heb ik een task gemaakt die het JSON bestand converteert naar primitieve, globale, css variabelen. Deze herschrijft elke value naar een lijst met keys:
Vervolgens pas ik de scripts in ./packages/stencil-library/package.json aan om altijd bij het runnen de stijlen te updaten:
Als we nu stencil starten, worden de stijlen geschreven en samengevoegd. Nu worden deze gebruikt in al onze componenten.
Semantic tokens.
De semantic tokens kunnen we opstellen in het bestand ./packages/stencil-library/src/globals/semantic-tokens.scss. En ook hiervoor gebruik ik het naamgeving systeem van SalesForce. Deze worden wederom opgebouwd als:
--
Dat is dus namespace arcady, niveau s voor ‘semantisch’, en bijvoorbeeld categorie color, en property primary. Het property, anders dan een ‘feitelijke observatie’ zoals welke kleur het is, beschrijft nu het doel van de waarde. Primary is onze hoofdkleur, als we daar onze groene kleur aan toewijzen geven we deze een context en doel in ons project. Andere voorbeelden van properties zijn bijvoorbeeld secondary, focus, border-radius. Adobe beschrijft deze laag die wij semantisch noemen als ‘alias’. Dus hebben we in onze globale tokens een waarde voor bijvoorbeeld spacing-md, maken we een alias voor een standaard border radius met deze maat:
--arcady-s-border-radius-md: var(--arcady-g-spacing-md)
Op die manier maken we alle semantische tokens aan voor dit project, alle vaste keuzen die de designer gemaakt heeft.
Het is goed om te onthouden dat er niet één juiste manier is. Probeer zo goed mogelijk mee te gaan in de manier waarop de designer het design system gemaakt heeft, of ga vooraf met elkaar in gesprek. Zo zorg je ervoor dat wijzigingen makkelijk aan te brengen zijn. Spreek met elkaar af wat in het design system globale en semantische tokens zijn.
Dat was het, nu heb je het design system geïmplementeerd in je project. Dus kunnen we componenten gaan bouwen. Voor dit artikel schrijven we samen een button component - op basis daarvan kun je zelf de rest van je componenten bouwen.
Button component.
Allereerst verwijderen we het standaard component, ./packages/stencil-library/src/components/my-component. Omdat een web component altijd een - moet bevatten in de naamgeving, is het slim om een standaard prefix te hanteren voor je project, bijvoorbeeld de naam van je namespace, in dit voorbeeld gebruik ik arcady
In de terminal voer je vervolgens dit commando uit om een button te scaffolden: npx stencil g arcady-button en scaffold ik met alle opties: Stylesheet, Spec test, E2E test.
Als eerste hernoem ik arcady-button.css naar arcady-button.scss en pas ik dat aan in arcady-button.tsx. Nu ons eerste component bestand is geopend, is het een mooi moment om eens te kijken hoe Stencil.js werkt.
Ieder component is een TypeScript class, met een Component decorator. Die decorator is afkomstig uit Stencil en zorgt dat de klasse kan worden geconverteerd naar een native web component. Standaard zijn drie opties ingevuld:
tag: de naam van je nieuwe HTML element. styleUrl: de locatie van je (S)CSS bestand. shadow: boolean die bepaald of de shadowDOM gebruikt wordt. Gaat je component kunnen updaten tijdens de runtime? Zet deze dan op true. Voor browser compatability voegt Stencil automatisch polyfills toe.
Verder bevat de class een render methode, de return hiervan geeft je element terug. Andere functies en mogelijkheden komen we gaandeweg tegen, voor geavanceerde functies die we niet bespreken kun je terecht in de documentatie van Stencil.
In arcady-button.tsx maakte ik een simpele knop die voldoet aan alle gangbare A11Y eisen en drie verschillende stijlen heeft.
De @Prop decorator gebruik je om aan te geven dat een component properties ontvangt en welk type deze dan kunnen zijn. Dit betekent dat ik straks in mijn code bijvoorbeeld een variant kan toevoegen aan de button:
Het toevoegen van stijlen op deze manier lijkt op het eerste gezicht vrij overweldigend en complex, maar als je beter kijkt zie je dat er eigenlijk slechts variabelen worden instgesteld en later worden uitgelezen. Ik nodig je uit om de code eens te bekijken en er vervolgens samen doorheen te lopen:
Allereerst specificeer ik de ‘default’ variabelen die ik gebruik om het component te stijlen. Deze geef ik een prefix --_ om duidelijk te maken dat deze ‘private’ of ‘local’ zijn. Dit is absoluut geen must, maar zo maak je heel snel aan jezelf duidelijk waar deze stijl vandaan komt. Dit wijkt af van de naamgeving van Salesforce, zij specificeren alle component tokens iets uitgebreider en mogelijk op een globaal niveau. Hier wijk ik vanaf omdat ik graag vrijheid per component wil houden. SalesForce hanteert de volgende naamgeving voor component tokens:
--
Waar namespace je project is, in mijn geval arcady. Het niveau is component dus c. Categorie is bijvoorbeeld color. Property is bijvoorbeeld background. Dan is element het component waarvoor je tokens definieert. In mijn geval button. Het type is wat ik variant noem, dus bijvoorbeeld primary. En state is de staat van het component, bijvoorbeeld active, disabled of hover.
Omdat je binnen Stencil al een specifiek (s)css bestand schrijft voor je component, sla ik een groot deel van de naamgeving over zodat de namen niet te lang worden en binnen het component toch kraakhelder is waar deze voor dient. Ik duid deze aan als volgt: --_
Mijn doel is om de waarde component uniek te houden binnen mijn project. Een goede manier hiervoor is om simpelweg je tagname te gebruiken die je voor dit component hebt gespecificeerd: die moet immers al uniek zijn.
Visual testing.
Nu we een component hebben gebouwd, kunnen we deze gaan testen. In de simpelste vorm, test je een component door hem toe te voegen aan de index.html van je Stencil project - niet ideaal omdat je de bron aan moet passen om te kunnen testen. In plaats daarvan opteer ik om een Story aan te maken voor ieder component.
In de map van mijn component, ./packages/stencil-library/src/components/arcady-button maak ik een extra bestand aan: arcady-button.stories.tsx. Dit bestand geeft instructies aan Storybook met wat voor een component het te maken heeft, welke opties deze heeft en laadt het voor ons in een interface.
In arcady-button.stories.tsx leggen we allereerst een export vast, dit bepaald hoe het component geordend wordt in Storybook. In dit geval maak ik een map ‘Components’ met daarin het component ‘ArcadyButton’.
Vervolgens stel je het template op, dit is een functie die je component teruggeeft met alle properties die je deze hebt gegeven. Vervolgens schrijf ik drie verschillende varianten van mijn button met verschillende argumenten, zo worden deze drie varianten ook in Storybook weergeven. Voer npm run storybook uit om Storybook te openen en je componenten te bekijken.
Dit is de absolute basis van Storybook, en het heeft nog veel meer in petto. Bekijk eens de tutorials van Storybook om er meer uit te halen https://storybook.js.org/tutorials/.
Unit testen.
Wanneer je een nieuw component genereert, wordt er ook een map
Na het generen ziet de unit test er als volgt uit:
In het predicaat “it renders”, wordt gespecificeerd hoe de HTML eruit zou moeten zien na het renderen van de button. Omdat we het element al hebben aangepast zal deze test falen. Een goed idee dus om deze test weer werkend te maken in de meest elementaire vorm van het component: een button, zonder tekst, zonder attributen. We weten uit ons component dat we een Host met daarin een button met daarin een slot renderen. Laten we dat in de test zetten:
Voeren we nu de test uit (npm run test) zien we dat ze allemaal slagen. Alleen test de test nu vrij weinig. Laten we testen of er iets veranderd als we attributen toevoegen:
Met deze test, testen we of de variant op de juiste manier doorgegeven wordt aan de button in de shadow-root. Door meer van dit soort testen toe te voegen, voorkom je dat de output ongewenst veranderd als de logica in je componenten aangepast wordt. Dit is misschien minder relevant voor een, qua logica, simpel component zoals een button, maar complexere componenten zouden de verkeerde output kunnen geven. Het toevoegen van een test vangt dit af. Meer weten over unit testing in Stencil? Bekijk de docs: https://stenciljs.com/docs/unit-testing.
End to end testing.
Ons button component bevat ook functionaliteit, namelijk een click event. Om te valideren dat deze ook werkt, kunnen we end-to-end testing inzetten om een click op onze button te simuleren.
End to end testing kun je gebruiken om te valideren dat methoden in je component worden aangeroepen na een actie, of dat een prop een specifieke value heeft. Stencil heeft hier uitgebreide documentatie over, lees die hier: https://stenciljs.com/docs/end-to-end-testing.
Naast de default test in het e2e bestand, die controleert of component juist laadt en een ‘hydrated’ class toegekend krijgt van Stencil, een toe om te verifiëren dat het click event uitgezonden wordt.
Documentatie.
De documentatie in Docusaurus schrijf je in Markdown of React. Ik kies hier voor Markdown, en nog specifieker voor MDX, een versie waar je ook javascript in kan schrijven. De documentatie van mijn button component is opgebouwd uit drie delen: doel van het component met een voorbeeld, gebruik van het element en de properties van het element. Probeer hier zo descriptief mogelijk te zijn, dit helpt gebruikers om de componenten zo goed mogelijk te gebruiken.
Hieronder een voorbeeld van mijn documentatie:
Let op: Omdat de code-preview niet juist laadde, heb ik ``` vervangen met '
In de docs map kun je het commando npm run start uitvoeren om je docs in de browser te bekijken.
Let's launch to Azure.
Ik wil het project publiceren naar Azure, zodat mijn organisatie zelf kan beheren wie wel en wie geen toegang krijgt tot mijn package. Eerst gaan we ons package publiceren vanuit onze command line, zodat we begrijpen hoe het werkt en of het werkt. Later maken we ook een productie pipeline in Azure om dit process te automatiseren.
First time setup.
Als eerste navigeren we naar de map met het Stencil project, dit is het project dat we willen publiceren. cd component-library-demo/packages/stencil-library/. Open vervolgens je package.json en controleer je instellingen. Name is de naam van je package, deze moet uniek zijn binnen je registry (de Azure omgeving), prefix deze bijvoorbeeld met @
Open nu in je browser Azure DevOps, open je project en klik in de sidebar op ‘Artifacts’. Waarschijnlijk heeft je project nog geen feeds - dat zijn een soort outputkanalen voor je project - klik dus bovenaan op “create feed”. Geef je nieuwe feed een naam die duidelijk maakt aan welk project het verwant is en check ‘upstream sources’. De rest van de instellingen zijn aan jou, deze hebben te maken met de toegankelijkheid voor derden.
Na het aanmaken van je feed, klik je bovenin op “Connect to feed”, kies hier ‘npm’. Klik vervolgens op je operating system en volg de stappen die zijn aangegeven. Uiteindelijk heb je, ten minste, één .npmrc bestand aangemaakt. Let wel op dat je dit in de map met Stencil toevoegt en niet in de monorepo.
Na de project setup voer je npm publish uit om je project te publiceren. Herlaad de pagina, en daar is het: jouw component library.
Gebruiken van je package.
Aan de documentatie willen we nog de echte versie van het project toevoegen. Als eerste kopieer je het .npmrc uit ./packages/stencil-library/ naar ./docs. Deze bevat geen vertrouwelijke informatie, maar wijst NPM naar de plek waar het packages vandaan moet halen: jouw Azure DevOps omgeving. Kopieer vervolgens het npm install commando van de package pagina in Azure DevOps en voer deze uit in ./docs.
Nu in ./docs/component-library-loader.js kunnen we het project vanuit de package inladen, in plaats van via de lokale omweg. Ik pas de imports aan naar:
Start je docs, npm run start en je zult zien: de knop doet het nog!
CI/CD.
Nu, om automatisch het package en de docs te updaten, maken we een nieuw bestand: ./pipelines/publish-packages.yaml. De stappen in deze pipeline zijn een voor een, terug te herleiden naar de handmatige deployment - alleen nu als build stappen. Wel moeten er nog extra rechten worden toegekend aan je build-agent in Azure. Ga hiervoor in DevOps naar: je project → Artifacts → het artifact feed → Feed settings (tandwiel rechtsboven) → Permissions. Verwijder vervolgens ‘ Build Service ()’ en voeg deze opnieuw toe met de ‘contributor’ role.
Om problemen tijdens de deployment te voorkomen, voor je de volgende wijzigingen uit in de package.json in de root van je project.
Het laatste wat je rest te doen, is mergen naar je main branch en het updaten begint vanzelf. Packages worden alleen geüpdate als de version in de individuele package.json hoger zijn dan die van de vorige versie.
Om je documentatie, los van de packages, te kunnen updaten maken we hiervoor een losse pipeline. Dit omdat de build stopt wanneer er geen version change is. Maak nu een bestand ./pipelines/publish-docs.yaml en vul deze met:
Om de deploy token te kunnen invullen, hebben we eerst een service nodig. Ga hiervoor naar portal.azure.com. In het kort volgen hier de stappen voor het aanmaken van je app.
In de Azure portal, klik op je subscription. Kies vervolgens in de linkerzijde voor Resource groups en kies de resource group die je wil gebruiken, of maak een nieuwe aan. Klik vervolgens bovenin op ‘Create’ en kies voor “Static Web App”. De informatie, zoals naam, die je in moet vullen maakt hier niet uit. Kies als “Source” Other, omdat we deze later instellen.
Vanuit je resource group open je nu de Static Web App en klik je bovenin op “Manage deployment token”, kopieer deze. Ga nu terug naar je project in DevOps en kies links onder Pipelines voor Library. Maar hier een nieuwe variable group, geef deze een naam en sla op (bijvoorbeeld die van je project). Ten slotte maak ik een nieuwe variabele aan met de naam azure_web_static_components_demo_deployment_token en geef als waarde de deploy token die we net hebben gekopieerd.
Push nu je nieuwe pipelines. Onder pipelines in DevOps kies je ‘New pipeline’, ‘Azure Repos Git’, en kies vervolgens de pipelines die je net hebt toegevoegd. De volgende keer dat je naar main merged, zal alles automatisch geupdatet worden!
Conclusie.
Bij het realiseren van een design system is het goed om af te wegen of een component library goed past bij jouw project(en). Nu heb je een goed idee wat daarvoor nodig is, welke complexiteit daarbij komt kijken en natuurlijk welke wist er potentieel voor het oprapen ligt.
Door het volgen van een gestructureerde aanpak en het gebruik van design systemen kan jij nu ook component libraries bouwen die overal werken. Je leerde onder andere het definiëren van variabelen, het ontwikkelen van visuele testen en het implementeren van unit- en end-to-end testen zijn essentiële stappen in dit proces. Daarnaast weet je dat het belangrijk is om goede documentatie te bieden en het publiceren van de component library in een gecureerde omgeving zoals Azure kan de toegankelijkheid en beheersbaarheid vergroten.
Bekijk de repo!
https://github.com/DemienDrost/component-library-demo
Tip: Gebruik dit project als template voor jouw project!
Bronnen:
https://atomicdesign.bradfrost.com/chapter-2/
https://www.figma.com/blog/the-future-of-design-systems-is-semantic/
https://sparkbox.com/foundry/designsystemmakeupdesignsystemlayerspartsofadesignsystem
https://m3.material.io/foundations/design-tokens/how-to-use-tokens
https://www.designsystems.com/how-spotifys-design-system-goes-beyond-platforms/
https://developer.mozilla.org/en-US/docs/Web/API/Web_components
https://www.arcady.nl/updates/web-components-101-part-1/
https://stenciljs.com/docs/introduction
https://design-tokens.github.io/community-group/format/
https://bootcamp.uxdesign.cc/what-are-design-tokens-828c67410069
https://dev.to/azure/11-share-content-with-docusaurus-azure-static-web-apps-30hc