Web components. Een term die steeds vaker terugkomt. Maar net als veel andere tech-hypes lijkt de belofte van web components nog niet waar gemaakt te zijn. Of toch wel? Web components zijn al lang niet meer zo obscuur als een paar jaar geleden. Alleen in Nederland al gebruiken een aantal grote organisaties al meerdere jaren web components in productie. Steeds meer teams herkennen de meerwaarde van de onafhankelijkheden die webstandaarden met zich meebrengen.
Echter, vind ik dat ze nog niet altijd in even goed daglicht staan! Daarom neem ik je de komende weken mee in het gedachtegoed, de basics en het ecosysteem, en laat ik je zien hoe je een volwaardige frontend applicatie kan bouwen met web components.
Wat zijn web components?
Frontend features zijn veelal gemaakt met een mix van HTML, CSS en JavaScript. En zoals iedereen weet is het wél zo netjes om dit op zo’n manier te maken, dat je dit ook op meerdere plekken kan hergebruiken. Nu is dit tegenwoordig hartstikke makkelijk met de meeste frontend frameworks als Angular, React en Vue. Je kan alle aspecten van een component in één bestand beschrijven, en heel erg makkelijk dit component op andere plekken importeren en hergebruiken.
Echter is dat gemak van het delen van componenten helemaal niet zo makkelijk binnen native web development. Je HTML staat vaak in een HTML-bestand, je CSS in een CSS-bestand en je JavaScript in een JavaScript bestand. Je kan in JavaScript wel meer scheiding bereiken, bijvoorbeeld door dynamisch HTML-snippets op te bouwen en CSS in te laden wanneer nodig. Maar écht zo makkelijk als met de bekende frontend frameworks is niet.
Toch is daar de afgelopen tijd verandering in gekomen. De komst van web components is een stille revolutie in deze ruimte van herbruikbare componenten. De term “Web components” is eigenlijk een verzamelnaam voor een webstandaarden: Custom Elements, Shadow DOM en HTML Templates. Samen geven deze standaarden de mogelijkheid om web components te bouwen.
Frontend development met web components is vergelijkbaar met bare metal development, maar dan in de browser. Je bouwt interactieve frontend componenten direct bovenop webstandaarden, zonder dat daar nog een framework of runtime tussen zit. Web components zijn dus geen framework, library of andere afhankelijkheid die je via NPM kan installeren.
Genoeg droge stof! Om deze concepten verder te ontdekken, zal ik ze alle drie even laten zien met een paar praktische voorbeelden.
Custom Elements
Zoals de naam impliceert, beschrijft de Custom Elements specificatie het definiëren van nieuwe maatwerk elementen. Dit zijn HTML-elementen, zoals de standaard <p>, <video> en <select> tags. Met de CustomElementRegistry kan je nu ook jouw eigen elementen definiëren.
class CorporateHeader extends HTMLElement { constructor() { super(); this.innerHTML = ` <h1>Welcome to our website!</h1> <a href="...">Click here to learn more</a> `; } } customElements.define('corporate-header', CorporateHeader);
De code laat een klasse zien die overerft van HTMLElement. In de constructor wordt vervolgens een stukje HTML ingeladen in de innerHTML property. Deze herken je mogelijk wel uit de tijd van JQuery en intensief gebruik van de querySelector: het uitlezen en aanpassen van de inhoud van een DOM-node. Tot slot wordt de klasse als een Custom Element gedefinieerd met behulp van de CustomElementRegistry. Deze is beschikbaar via window.customElements, en met behulp van de define(<name>, <class>) functie kan je de klasse registreren om op een andere plek in de pagina te gebruiken.
Er zijn maar twee eisen: de klasse moet (uiteindelijk) overerven van de HTMLElement klasse, en de naam van het element moet bestaan uit minimaal twee woorden met een verbindingsstreepje ertussen, bijvoorbeeld app-drawer. De reden hiervoor is zodat de browser weet welke elementen standaard zijn, en welke Custom Elements zijn. Dit is handig voor forwards-compatibility: wanneer er nieuwe standaardelementen worden toegevoegd zullen die niet conflicteren met Custom Elements, omdat standaardelementen altijd één woord of karakter als naam hebben.
Ons component voldoet aan deze eisen, dus kunnen we hem gebruiken op de pagina. Een Custom Element gebruiken is vrij simpel: de JavaScript moet worden uitgevoerd, waarna de naam van het element als DOM-node in de HTML moet worden beschreven, bijvoorbeeld:
<body> <corporate-header></corporate-header> ... <script src="corporate-header.js"></script> </body>
Doordat we een klasse hebben gekoppeld aan de tag <corporate-header> met behulp van de CustomElementRegistry weet de browser dat een instantie van het Custom Element moet worden aangemaakt. De code in de constructor wordt uitgevoerd, en de HTML wordt toegevoegd aan de DOM-node van de header zodat de eindgebruiker het ook kan zien!
Dit is natuurlijk een simpel voorbeeld, maar de kracht van Custom Elements is al voorzichtig zichtbaar door het verbergen van de business & UI logica in één klasse met een simpele interface in de vorm van een HTML-tag.
Maar Custom Elements zijn op zichzelf nog niet zo spannend. Daarom gaan wel snel door met de volgende webstandaard!
Shadow DOM
Het afscheiden van HTML & CSS van verschillende componenten op een pagina (encapsulation) is al jaren een discussiepunt binnen frontend applicaties. Er zijn talloze methodieken bedacht om zo min mogelijk conflicten te hebben met styles, bijvoorbeeld met BEM, Atomic of SMACSS. Veel frontend frameworks hebben ook ondersteuning voor encapsulation via een VDOM-engine of andere emulatietechnieken om het uitlekken van styling te voorkomen.
Nu hoor ik je denken: “Als dit zo’n bekend probleem is, dan heeft de browser zelf toch ook wel wat te bieden hiervoor?” Dat klopt! Daarvoor kan je gebruik maken van de meest badass-klinkende term binnen web development: de Shadow DOM.
De Shadow DOM is eigenlijk een hele simpele constructie om encapsulation mogelijk te maken: naast de normale (light) DOM kunnen er meerdere DOM-trees bestaan. Deze zijn volledig afgezonderd, met als enige connectie een referentie met een shadow host element op de pagina. Alle HTML, CSS en ook JavaScript staat volledig los van de rest van webpagina. Dit geeft een niveau van afscheiding wat voorheen alleen mogelijk was met iframes, zonder de overhead van het inladen van een nieuwe webpagina.
Dat ziet er ongeveer uit als het diagram hierboven. De Document Tree is een normale HTML-pagina zoals je hem gewend bent, met divjes, paragrafen en hyperlinks. Echter is hier een DOM-node die anders is dan de rest: het is een Shadow Host. Dat is een DOM-node waar een Shadow Tree aan verbonden is. Die Shadow Tree staat in verbinding met de Document Tree door een koppeling tussen de Shadow Host & Root. Binnen de Shadow Tree kunnen vervolgens weer DOM-nodes aangemaakt worden. Het verschil: deze nodes zijn verborgen als implementatiedetails en worden ook niet aangetast door globale styling regels.
Oké, dat stukje van dat de styling binnen de Shadow DOM helemaal afgezonderd is van de Light DOM is misschien niet helemaal waar. Sommige styles zijn namelijk wél handig om te delen met alle Shadow DOM instanties op de pagina. Denk bijvoorbeeld aan color, background-color en font-family definities. Deze inherited CSS properties gelden ook voor alle shadow DOM content op de pagina. Da’s wel zo handig: anders zou alle HTML binnen de Shadow DOM een kale witte achtergrond zonder lettertypes hebben. Optioneel kan je het delen van deze properties uitzetten door all: initial; toe te voegen aan de styles binnen de Shadow DOM waar dit niet wenselijk is.
De browser moet natuurlijk wel de logische volgorde van de webpagina begrijpen. Daarom wordt de totale Light + Shadow DOM platgeslagen in een enkele boomstructuur. De browser houdt echter wel bij welke delen binnen specifieke Shadow Boundaries vallen, zodat daar bijvoorbeeld de styling kan worden afgezonderd. Maar door deze normale boomstructuur kan je bijvoorbeeld nog wel met JavaScript door nodes heen lopen en de DOM structuur aanpassen.
Het aanmaken van een nieuwe Shadow Tree kan in een paar regels JavaScript:
<div class="shadow"></div> <script> // Attach new shadow root to the div element const node = document.querySelector('.shadow'); node.attachShadow({ mode: 'open' }); // Use node.shadowRoot to manipulate the shadow DOM node.shadowRoot.innerHTML = '<p>Hello...</p>'; // You can also use node.shadowRoot.querySelector // and anything you usually use to manipulate DOM // Append a new <span> element to the shadow DOM of // the node element const message = document.createElement('span'); message.textContent = '...world!'; node.shadowRoot.appendChild(message); </script>
Als eerste wordt er met de querySelector een element uitgekozen om een Shadow Root aan toe te voegen met een aanroep naar attachShadow(…) op de DOM-node. Zodra de Shadow Root is aangemaakt, kan je er mee interacteren door de shadowRoot property te gebruiken van de node. Deze Shadow Root bevat veel herkenbare functionaliteit die je ook op de document zou aantreffen zoals shadowRoot.querySelector en shadowRoot.appendChild.
Het resultaat is dat het div-element een Shadow Host is geworden. In de developer tools kan je de Shadow Root openklappen om de inhoud te onthullen:
Er zit echter meer verborgen in de schaduwen! Shadow Roots hebben namelijk twee modes: open en closed. Met deze modes kan je de mogelijkheid in- of uitschakelen om te interacteren met de Shadow DOM van het host element. Wanneer de mode open is, kan je met JavaScript via de querySelector interacteren met de nodes binnen de Shadow DOM, bijvoorbeeld om data uit te lezen of om nieuwe elementen toe te voegen. Wanneer deze mode closed is, is er geen enkele interactie mogelijk van buitenaf. Dit zorgt dus voor volledige afzondering van de rest van de webpagina, en maakt het onmogelijk om van buitenaf de content, styles of functionaliteit aan te passen.
Er zit nog véel meer functionaliteit in de Shadow DOM wat te gedetailleerd is om in deze introductieblog op in te gaan. Zo kan je custom & browser events configureren om wél of níet door Shadow Boundaries te bubblen, Shadow Trees nestelen in andere Shadow Trees en native CSS-mixins en variabelen gebruiken om herbruikbare styling te maken voor gebruik binnen de Shadow DOM.
De Shadow DOM wordt al jaren volop gebruikt op elke webpagina. Complexe standaardelementen verbergen hun implementatie vaak in de Shadow DOM. Om dit te zien kan je in de (Chrome) Dev Tools “Show user agent shadow DOM” aanvinken. Wanneer je dan bijvoorbeeld input elementen bekijkt, zal je zien dat de Shadow DOM de details van de implementatie verbergt van de ontwikkelaar.
Pfoeh, dat was een hoop gebrabbel over Shadow-dittes en Shadow-dattes. Maar geen zorgen, we zijn er bijna. Het laatste onderdeel van de Web Components specificaties is namelijk HTML Templates!
HTML Templates
HTML Templates zijn eigenlijk niets meer dan herbruikbare HTML, CSS en JavaScript. Je kan met de <template> tag gemakkelijk zo’n snippet schrijven, bijvoorbeeld zo:
<template id="reusable-paragraph"> <style> p { color: white; background-color: #6f6f6f; padding: 8px; } </style> <p>This paragraph can be reused in lots of places efficiently.</p> </template>
Vervolgens kan je met een stukje JavaScript deze template opzoeken en klonen:
<div class="placeholder"></div> <template id="reusable-paragraph">...</template> <script> // Get the template and clone it const templateEl = document.getElementById('reusable-paragraph'); const cloned = templateEl.cloneNode(true); // Append the template content to the div element const placeholder = document.querySelector('.placeholder'); placeholder.appendChild(cloned.content); </script>
Het resultaat is dat de inhoud van de template is gedupliceerd en geplaatst in het div-element op de pagina:
Op zichzelf is dit nog niet zo spannend. Maar de kracht van HTML templates ligt in hoe ze onder de motorkap werken. Wanneer een <template> element namelijk in de pagina staat, wordt deze alleen maar geparsed door de browser. De inhoud van de template wordt echter niet getoond aan de eindgebruiker: deze blijft verborgen. Pas op het moment dat een template wordt geactiveerd door JavaScript code, komt de ware kracht naar boven. Door cloneNode() aan te roepen op een template-element, wordt alle inhoud vanuit de template efficiënt gedupliceerd, zodat deze kan worden hergebruikt in de pagina. De reden dat dit zo efficiënt kan is omdat de browser de inhoud van de template al van tevoren heeft geparsed.
Bringing it all together
Als je het volledige plaatje nog niet helemaal voor je ziet: geen probleem. Dit volgende voorbeeld laat zien hoe je deze drie standaarden kan combineren om een flexibel component kan bouwen dat herbruikbaar is.
Voor deze feature gaan we een simpele tegel bouwen voor een nieuwswebsite waar een titel, inhoud en wat links in kunnen staan. Elk artikel kan dit component gebruiken om dezelfde styling en functionaliteit te krijgen, maar natuurlijk wél met inhoud specifiek voor dat artikel.
We beginnen met het visuele stuk. Om een generieke HTML template voor deze feature te kunnen definiëren gebruik ik het <slot> element. Dat is een element dat je binnen templates mag gebruiken om aan te geven dat op díe plek HTML van buitenaf mag worden gezet. Ideaal dus voor onze artikel feature, waar de titel en tekst voor elk artikel uniek is.
<template id="news-card-template"> <style> .news-card { box-shadow: 0 10px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.19); border-radius: 10px; padding: 16px; margin: 32px; } </style> <div class="news-card"> <h2 class="title"><slot name="title">NEEDS TITLE</slot></h2> <p class="content"><slot name="content">NEEDS CONTENT</slot></p> <a href="...">Read More</a> <a href="...">Share</a> </div> </template>
Om deze template op meerdere plekken te gebruiken, zou je op elke plek een stukje JavaScript moeten schrijven om de template te clonen en daarna op de juiste plek in de DOM toe te voegen. Dat is handmatig werk wat we niet willen doen, en dus de ideale kandidaat om te combineren met een Custom Element.
class NewsCard extends HTMLElement { constructor() { super(); // Re-use the template in the <news-card> component const template = document.getElementById('news-card-template'); const shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.appendChild(template.content.cloneNode(true)); } } customElements.define('news-card', NewsCard);
De NewsCard klasse doet niet enorm veel, maar knoopt wel de template aan een herbruikbare definitie: de <news-card> tag. Ik heb gekozen om dit binnen de Shadow DOM te doen: ik wil namelijk niet dat het artikel beïnvloed wordt door styling van buitenaf. Andersom wil ik ook niet dat de styling binnen dit component andere zaken op de webpagina aantast.
Nu hoeven we het component alleen nog in de pagina te gebruiken.
<!-- Empty NewsCard --> <news-card></news-card> <!-- NewsCard with content! --> <news-card> <span slot="title">Hello World!</span> <ul slot="content"> <li>This</li> <li>is</li> <li>quality</li> <li>content.</li> </ul> </news-card>
Om goed te laten zien hoe de slots precies werken, heb ik een NewsCard zonder en mét content in de pagina gezet. Het resultaat mag er wezen!
Zoals je ziet worden de standaardwaarden van de slots binnen de eerste NewsCard gebruikt. Omdat de tweede NewsCard wél een inhoud heeft gekregen binnen de slots, wordt de content daar mooi weergeven. In de DOM Tree zien deze twee NewsCard instanties er zo uit:
De structuur van de webpagina lezen met het gebruik van de Shadow DOM en slots vereist wat handigheid en ervaring. Zo moet je de Shadow Root van de NewsCard uitklappen om de inhoud te zien. Wat ook opvalt bij de tweede NewsCard instantie is dat de inhoud van de slot niet in, maar juist naast de Shadow Root staat! Dat komt omdat de inhoud van de slot niet binnen de Shadow DOM wordt aangemaakt, maar juist van buitenaf op de plek waar het component wordt gebruikt. Doordat de <span> en <ul> van buitenaf aan het component worden meegegeven, ontstaat er een interessant mix van elementen binnen én buiten de Shadow DOM. De <span> en <ul> kunnen nog steeds gestyled worden door globale CSS, maar diezelfde CSS regels kunnen binnen de Shadow DOM verder niets aan het artikel aanpassen.
Het resultaat van het combineren van Custom Elements, Shadow DOM en HTML Templates is eigenlijk de oplossing voor de problemen waar ik eerder over schreef. In een paar regels HTML, CSS en JavaScript hebben we een herbruikbaar component gedefinieerd, die makkelijk kan worden ingeladen, én de Shadow DOM benut om encapsulation toe te passen.
Wat is hier uniek aan? Mijn favoriete framework kan dit ook, en iedereen kent dat al!
Oké, je eigen HTML-tags en DOM/Style encapsulation, dat is niks nieuws. Custom HTML-tags kan je gebruiken in vrijwel alle frontend frameworks. En DOM/Style encapsulation, dat is ook al een ingeburgerd begrip. Angular heeft dit al sinds de dagen van AngularJS, en ondersteunt zelfs Shadow DOM ViewEncapsulation out-of-the-box. Ook Vue heeft Scoped CSS en in React kan je met CSS-in-JS hetzelfde bereiken.
Tóch is dit net even wat anders. Waar frameworks hun eigen encapsulation emulatie moeten bouwen om deze features te ondersteunen, zijn Custom Elements, Shadow DOM en HTML Templates browserstandaarden. En het mooie van een browserstandaard is dat het in de browser zit ingebakken. Alles wat je tot nu toe hebt gezien werkt native in de browser, zonder enige afhankelijkheden van derde partijen. Én helemaal geen build configuratie! Dat scheelt dus Kilobytes die over de lijn gaan, en het scheelt in de algemene complexiteit van de applicatie gecombineerd met een onderliggend framework.
Mogelijk twijfel je nu nog een beetje. Misschien denk je dat dit een techniek is die zichzelf nog niet heeft bewezen in complexe praktijksituaties. Of zie je het nog niet helemaal voor je om overal templates te schrijven om die vervolgens met JavaScript aan een Custom Element te knopen.
Tóch ben ik van mening dat ook jij hier de waarde van kan inzien. Wil je weten hoe? Hou dan de socials van Arcady in de gaten, want volgende week laat ik je in deel twee van deze blogpost serie zien hoe je op een simpele manier met web components kan werken met behulp van web component libraries als Lit, Polymer en Stencil. Stay tuned!
Vier keer Good-to-Know!
Je kan Custom Elements ook gebruiken zonder Shadow DOM. Dan heb je een component dat HTML in de Light DOM plaatst, waardoor je alle HTML wél van buitenaf kan stylen. Dit kan bijvoorbeeld handig zijn voor herbruikbare componenten die je in meerdere applicaties wil gebruiken, met unieke styling binnen elke applicatie.
Wanneer je een systeem hebt gebouwd met web components, dan zal je moeten nadenken over hoe je bepaalde componenten update wanneer er brekende wijzigingen zijn. De CustomElementRegistry kan maar één component met een unieke naam bevatten, dus het is niet mogelijk om een oude én nieuwe implementatie te hebben voor een element. In een monorepository betekent dit dat alle implementaties meteen moeten worden aangepast. Wanneer de componenten in een losse NPM-package staan zal er moeten worden nagedacht over het toepassen van SemVer of het toepassen van een ScopedElements structuur.
Er is op dit moment weinig ondersteuning om web components in combinatie met Server Side Rendering te gebruiken. Het is namelijk niet mogelijk om shadow DOM statisch te genereren in een HTML-structuur die de browser kan inlezen. Er zijn wel meerdere voorstellen om dit mogelijk te maken, dus de verwachting is er ook dat dit binnenkort ondersteund zal worden.
Accessibility werkt zoals je zou verwachten met web components, en wanneer de shadow DOM wordt gebruikt dan zal bijvoorbeeld een screen reader gewoon in volgorde door de DOM structuur heen lopen. De regels die gelden voor semantisch HTML en WAI-ARIA gelden dus ook voor web components.
Deelnemen aan een Web Components workshop? Geef hier je interesse door. Ook beschikbaar voor developmentteams op locatie.
Inhoudsopgave