Shadow DOM
I förra artikeln såg du att en komponents konstruktor inte får lägga till barn på elementet självt. Ändå behöver nästan varje komponent ett inre: en knapp, en bild, några rubriker. Den här artikeln handlar om var det inre hamnar, shadow DOM, och om hur komponentens stilar och sidans stilar hålls isär.
Tidsåtgång: Ungefär 15 minuter.
Ett träd i trädet
Section titled “Ett träd i trädet”Du har redan använt element med ett dolt inre. Ett <video controls> har knappar för att spela och
pausa och ett reglage för volymen, men de syns inte bland elementets barn i utvecklarverktygen, och
document.querySelector('button') hittar dem inte. Knapparna ligger i elementets eget, inkapslade
träd.
Shadow DOM ger ditt element samma möjlighet. Elementet blir en shadow host, och till det kopplas en shadow root, roten i ett eget träd:
constructor() { super() this.attachShadow({ mode: 'open' }) this.shadowRoot.append(template.content.cloneNode(true))}Att koppla på en shadow root räknas inte som att lägga till barn på elementet, så det är tillåtet i
konstruktorn. Med mode: 'open' når du roten via this.shadowRoot, och det gör även kod utanför
komponenten. Det finns också mode: 'closed', där shadowRoot ger null utifrån, men det skyddar
inte mot något på riktigt och gör komponenten svårare att testa. Kursens komponenter använder
'open'.
I utvecklarverktygen syns shadow root som #shadow-root (open) direkt under elementet. Öppna den, så
ser du komponentens inre.
Mallen och stilarna
Section titled “Mallen och stilarna”Komponentens HTML och CSS skrivs som vanlig HTML och CSS, i egna filer. Så här ser user-card ut:
<article> <h3 class="name">Unknown</h3> <slot></slot></article>:host { display: block;}
h3 { margin: 0;}import htmlText from './user-card.html?raw'import cssText from './user-card.css?inline'
const template = document.createElement('template')template.innerHTML = htmlText
const stylesheet = new CSSStyleSheet()stylesheet.replaceSync(cssText)
export class UserCard extends HTMLElement { #name
constructor() { super()
this.attachShadow({ mode: 'open' }) this.shadowRoot.adoptedStyleSheets = [stylesheet] this.shadowRoot.append(template.content.cloneNode(true))
this.#name = this.shadowRoot.querySelector('.name') }
// ...}Det är samma uppbyggnad som komponenterna i kursens övningsuppgifter. ?raw och ?inline får Vite
att importera filernas innehåll som strängar. Varför kursen gör så, och vilka alternativen är, tar
nästa artikel upp.
Lägg märke till vad som körs var:
- En gång, när modulen laddas: mallen skapas från strängen, som i
kursvecka 9, och CSS-koden
tolkas till ett
CSSStyleSheet. - En gång per element, i konstruktorn: mallen klonas in i elementets shadow root, och
stilmallen kopplas på med
adoptedStyleSheets.
Alla <user-card> på sidan delar alltså på samma stilmallsobjekt i stället för att var och en får
en egen kopia av CSS-koden som måste tolkas. Du kan också lägga CSS-koden i ett <style>-element i
mallen. Det fungerar, men då tolkas den på nytt för varje element.
Inkapslingen
Section titled “Inkapslingen”Shadow-trädet är skilt från resten av dokumentet:
document.querySelector()söker inte in i shadow-träd. Komponenten söker i sitt eget träd medthis.shadowRoot.querySelector().- CSS i komponenten gäller bara i komponenten. Regeln
h3 { margin: 0; }ovan påverkar inte någon annan<h3>på sidan. - Sidans CSS gäller inte heller inuti komponenten. En regel som
h3 { color: red; }i sidans stilmall når inte rubriken iuser-card. - Ett
idi komponenten behöver bara vara unikt inom komponenten.
Det är därför du kan använda korta, enkla selektorer som h3 och button i en komponent utan att
tänka på resten av sidan.
Fråga: vilken färg får texten?
Section titled “Fråga: vilken färg får texten?”Sidans stilmall innehåller:
body { color: darkblue; font-family: Georgia, serif;}
h3 { color: red;}Vilken färg och vilket typsnitt får rubriken inuti user-card, där komponentens egen CSS inte sätter
någon färg eller något typsnitt?
Rubriken blir mörkblå och får typsnittet Georgia, inte röd.
Regeln h3 { color: red; } är en selektor, och sidans selektorer når inte in i shadow-trädet. Men
egenskaper som ärvs, som color och font-family, ärvs genom gränsen. Shadow-trädet ärver från sin
shadow host, <user-card>, som i sin tur ärver från <body>.
Det är en medveten del av standarden: en komponent ska smälta in i sidans typografi, men inte gå sönder av sidans selektorer.
Att styla komponenten
Section titled “Att styla komponenten”Inuti komponenten väljer du elementet självt, shadow host, med :host. Ett custom element är som
standard ett inline-element, som <span>. Därför börjar nästan varje komponents CSS med:
:host { display: block;}
:host([hidden]) { display: none;}Den andra regeln behövs eftersom display: block annars tar över webbläsarens standardregel för
attributet hidden. :host() med en selektor inom parentes matchar bara när elementet självt
matchar selektorn, här när det har attributet hidden.
Sidan kan fortfarande styla elementet självt utifrån, till exempel med user-card { margin: 1em; }.
Sätter både sidan och :host samma egenskap vinner sidans regel, så användaren av komponenten har
sista ordet.
Att öppna upp för stil utifrån
Section titled “Att öppna upp för stil utifrån”Ibland vill du låta den som använder komponenten styla något i den. Det finns två sätt, och båda bygger på att komponenten själv bestämmer vad som får ändras.
CSS custom properties (CSS-variabler) ärvs genom gränsen, precis som color. Komponenten
använder en variabel med ett standardvärde:
h3 { color: var(--user-card-name-color, inherit);}/* the page's stylesheet */user-card { --user-card-name-color: darkred;}::part() låter sidan styla ett helt element i komponenten, som komponenten har märkt med
attributet part:
<h3 class="name" part="name">Unknown</h3>/* the page's stylesheet */user-card::part(name) { font-variant: small-caps;}En CSS-variabel exponerar ett enskilt värde, en part ett helt element. Övningsuppgiften bart-board
i kursvecka 11 exponerar sin text som part="text".
Slots: innehåll från användaren
Section titled “Slots: innehåll från användaren”En komponent med en mall visar alltid samma struktur. Ofta vill du att den som använder komponenten ska kunna skicka in eget innehåll, som barn till elementet:
<user-card name="Ada Lovelace"> <p>Wrote the first published computer program.</p></user-card>Barnen hamnar i dokumentets vanliga träd, inte i shadow-trädet. För att de ska visas måste mallen
innehålla ett <slot>-element, en platshållare som talar om var de ska visas. Mallen för
user-card har en sådan efter rubriken, så stycket visas där.
Med attributet name kan en mall ha flera named slots. Innehållet väljer plats med attributet
slot:
<article> <h3><slot name="name">Unknown</slot></h3> <slot></slot></article><user-card> <span slot="name">Ada Lovelace</span> <p>Wrote the first published computer program.</p></user-card>Barn med slot="name" hamnar i slotten med samma namn, och resten i slotten utan namn. Innehållet
mellan slot-taggarna i mallen, Unknown, visas om inget skickas in.
Diagrammet visar det viktiga: de inskickade elementen flyttas inte. De är fortfarande barn till
<user-card> i dokumentets träd, och visas bara på slottens plats.
Fråga: hittar sidan elementet?
Section titled “Fråga: hittar sidan elementet?”Med HTML-koden ovan, vad ger de här två raderna?
document.querySelector('user-card span')document.querySelector('user-card h3')Den första ger <span slot="name">, den andra null.
<span> är ett vanligt barn till <user-card> i dokumentets träd. Att det visas inuti komponentens
<h3> ändrar inte var det finns. <h3> ligger däremot i shadow-trädet, dit document.querySelector()
inte når.
Samma sak gäller CSS: sidans stilmall kan styla <span> som vanligt, eftersom det är sidans element.
::slotted()
Section titled “::slotted()”Inuti komponenten kan du styla det inskickade innehållet med ::slotted():
::slotted(p) { margin-block: 0.5em 0;}::slotted() når bara de element som själva skickats in, inte deras barn. Och eftersom innehållet
är sidans element vinner sidans CSS om båda sätter samma egenskap.
Deklarativ shadow DOM
Section titled “Deklarativ shadow DOM”Allt ovan skapar shadow-trädet med JavaScript. Sedan 2024 kan det även skrivas direkt i HTML, med
<template shadowrootmode="open"> som första barn till elementet. Det används när sidan genereras
på en server, så att komponenten syns innan JavaScript har körts. Du behöver det inte i kursen, där
all HTML skapas i webbläsaren, men du kan stöta på det när du söker information.
Att ta med sig
Section titled “Att ta med sig”- En komponents inre läggs i en shadow root, som skapas med
this.attachShadow({ mode: 'open' })i konstruktorn. - Mallen och stilmallen skapas en gång per modul. Varje element klonar mallen och delar stilmallen
via
adoptedStyleSheets. - Selektorer når inte genom gränsen åt något håll, men ärvda egenskaper och CSS-variabler gör det.
:hoststylar elementet självt. Ge detdisplay: blockoch glöm inte:host([hidden]).- Sidan kan styla det komponenten exponerar, med CSS-variabler och
::part(). <slot>visar elementets barn på en bestämd plats i shadow-trädet, men barnen ligger kvar i dokumentets träd.
Vidare läsning
Section titled “Vidare läsning”- Läsanvisningarna för veckan, i synnerhet kapitel 15.6.4 i kursboken.
- Using shadow DOM på MDN.
- Using templates and slots på MDN.
- ::part() och :host på MDN.