Vi har alla projekt som vi gärna inte vill jobba på. Koden har blivit ohanterlig, omfattningen ändrats, snabba korrigeringar tillämpas ovanpå andra korrigeringar, och strukturen kollapsade under dess vikt av spaghettikod. Kodning kan vara en ren röra ibland.

Projekt underlättas av att använda enkla, oberoende moduler med ett enda ansvar. Modulär kod är inkapslad, så det finns mindre behov av att oroa sig för genomförandet. Så länge du vet vad en modul kommer mata ut när de får en uppsättning inmatningar behöver du inte nödvändigtvis förstå hur den uppnådde det målet.

Att använda modulära begrepp på ett enda programmeringsspråk är enkelt, men webbutveckling kräver en mångsidig blandning av teknik. Webbläsare tolkat HTML, CSS och JavaScript för att rendera sidans innehåll, stilar och funktionalitet.

De leker inte alltid snällt med varandra eftersom:

  • Relaterad kod kan delas mellan tre eller flera filer, och
  • Globala stilar och JavaScript-objekt kan störa varandra på oväntade sätt.

Och detta problem är utöver de som uppstår på grund av språkens körtider, ramverk, databaser och andra beroenden som används på servern.

Kolla in vår videoguide till webbkomponenter

Vad är webbkomponenter?

En webbkomponent är ett sätt att skapa ett inkapslat kodblock med ett enda ansvar som kan återanvändas på vilken sida som helst.

Tänk på HTML-taggen <video>. Men en webbadress kan en tittare använda kontroller som spela, pausa, gå tillbaka, gå framåt och justera volymen.

Styling och funktionalitet tillhandahålls även om du kan göra ändringar med olika attribut och JavaScript API-anrop. Valfritt antal <video>-element kan placeras inuti andra taggar, och de kommer inte att skapa konflikter.

Tänk om du behöver din egen anpassade funktionalitet? Till exempel ett element som visar antalet ord på sidan? Det finns ingen HTML-tagg för <wordcount> (ännu).

Ramverk som React och Vue.js låter utvecklare skapa webbkomponenter där innehåll, styling och funktionalitet kan definieras i en enda JavaScript-fil. Detta löser många komplexa programmeringsproblem, men kom ihåg:

  • Du måste lära dig att använda ramverket och uppdatera din kod när det utvecklas.
  • En komponent som skrivs för ett ramverk är sällan kompatibelt med ett annat.
  • Ramverk stiger och avtar i popularitet. Du kommer vara beroende av utvecklingsteamet och andra användares prioriteringar.
  • Standardwebbkomponenter kan lägga till webbläsarfunktionalitet vilket är svårt att uppnå i endast JavaScript (såsom Shadow DOM).

Lyckligtvis brukar populära begrepp som införts i bibliotek och ramverk hitta vägen in i webstandarder. Det har tagit lite tid, men webbkomponenterna har kommit för att stanna.

En kort historik över webbkomponenter

Efter många leverantörsspecifika halvstartar introducerades begreppet standardwebbkomponenter först av Alex Russell vid Fronteers Conference 2011. Googles Polymer-bibliotek (en polyfill baserad på de nuvarande förslagen) anlände två år senare, men tidiga implementeringar visade sig inte i Chrome och Safari förrän 2016.

Webbläsarleverantörer tog tid på sig att förhandla om detaljerna, men webbkomponenter lades till Firefox 2018 och Edge 2020 (när Microsoft bytte till Chromium-motorn).

Förståeligt nog har få utvecklare varit villiga eller kapabla att anta webbkomponenter men vi har äntligen nått en bra nivå av webbläsarstöd med stabila API:er. Allt är inte perfekt men de är ett alltbättre alternativ till ramverksbaserade komponenter.

Även om du inte är villig att dumpa din favorit ännu är webbkomponenter kompatibla med alla ramverk, och API:erna kommer att stödjas i många år framöver.

Förråd av färdiga webbkomponenter är tillgängliga för alla att ta en titt på:

… men att skriva din egen kod är roligare!

Denna guide ger dig en fullständig introduktion till webbkomponenter som skrivs utan ett JS-ramverk. Du kommer att få lära dig vad de är och hur du anpassar dem för dina webbprojekt. Du behöver lite kunskap om HTML5, CSS och JavaScript.

Kom igång med webbkomponenter

Webbkomponenter är anpassade HTML-element som <hello-world></hello-world>. Namnet måste innehålla ett streck för att aldrig kollidera med element som officiellt stöds i HTML-specifikationen.

Du måste definiera en ES2015-klass för att styra elementet. Det kan heta vad som helst, men HelloWorld är vanligt. Det måste förlänga HTMLElement-gränssnittet, som representerar standardegenskaperna och metoderna för varje HTML-element.

Observera: Firefox låter dig utöka specifika HTML-element såsom HTMLParagraphElement, HTMLImageElement, eller HTMLButtonElement. Detta stöds inte i andra webbläsare och låter dig inte skapa en Shadow DOM.

För att göra något användbart kräver klassen en metod som heter connectedCallback() som anropas när elementet läggs till i ett dokument:

class HelloWorld extends HTMLElement {

  // connect component
  connectedCallback() {
    this.textContent = 'Hello World!';
  }

}

I det här exemplet är elementets text inställd på ”Hello World”.

Klassen måste registreras hos CustomElementRegistry för att definiera den som en hanterare för ett visst element:

customElements.define( 'hello-world', HelloWorld );

Webbläsaren associerar nu <hello-word>-elementet med din HelloWorld-klass när JavaScript laddas (t.ex. <script type="module" src="./helloworld.js"></script>).

Du har nu ett anpassat element!

CodePen demonstration

Denna komponent kan formateras i CSS som alla andra element:

hello-world {
  font-weight: bold;
  color: red;
}

Lägga till attribut

Denna komponent är inte så användbar eftersom samma text matas ut oavsett. Liksom alla andra element kan vi lägga till HTML-attribut:

<hello-world name="Craig"></hello-world>

Detta kan åsidosätta texten så att ”Hej Craig!” visas istället. För att uppnå detta kan du lägga till en constructor()-funktion till HelloWorld-klassen, som körs när varje objekt skapas. Den måste:

  1. Anropa super()-metoden för att initiera de överordnade HTMLElement, och
  2. gör andra initieringar. I det här fallet definierar vi en namn-egenskap som är inställd på ”World”:
class HelloWorld extends HTMLElement {

  constructor() {
    super();
    this.name = 'World';
  }

  // more code...

Din komponent bryr sig bara om name-attributet. En statisk observedAttributes()-egenskap bör returnera en rad egenskaper att observera:

// component attributes
static get observedAttributes() {
  return ['name'];
}

En attributeChangedCallback()-metod anropas när ett attribut definieras i HTML eller ändras med JavaScript. Det har skickat egenskapsnamnet, gammalt värde och nytt värde:

// attribute change
attributeChangedCallback(property, oldValue, newValue) {

  if (oldValue === newValue) return;
  this[ property ] = newValue;

}

I detta exempel skulle endast name-egenskapen någonsin uppdateras, men du kan lägga till ytterligare egenskaper vid behov.

Slutligen måste du justera meddelandet i connectedCallback()-metoden:

// connect component
connectedCallback() {

  this.textContent = `Hello ${ this.name }!`;

}

CodePen demonstration

Livscykelmetoder

Webbläsaren anropar automatiskt sex metoder under hela webbkomponenttillståndets livscykel. Den fullständiga listan finns här även om du redan har sett de första fyra exemplen ovan:

constructor()

Den anropas när komponenten först initieras. Den måste anropa super() och kan ställa in eventuella standardvärden eller utföra processer innan rendering.

static observedAttributes()

Returnerar en rad attribut som webbläsaren kommer att observera.

attributeChangedCallback(propertyName, oldValue, newValue)

Anropas när ett observerat attribut ändras. De som definieras i HTML skickas omedelbart, men JavaScript kan ändra dem:

document.querySelector('hello-world').setAttribute('name', 'Everyone');

Metoden kan behöva utlösa en om-rendering när detta inträffar.

connectedCallback()

Denna funktion anropas när webbkomponenten läggs till i en dokumentobjektmodell (DOM). Den borde göra all nödvändig rendering.

disconnectedCallback()

Den anropas när webbkomponenten tas bort från en dokumentobjektmodell. Detta kan vara användbart om du behöver städa upp, till exempel att ta bort ett lagrat tillstånd eller avbryta Ajax-förfrågningar.

adoptedCallback()

Den här funktionen anropas när en webbkomponent flyttas från ett dokument till ett annat. Det kan finnas en användning för detta, även om jag inte själv kommer på en!

Så interagerar webbkomponenter med andra element

Webbkomponenter erbjuder några unika funktioner som du inte hittar i JavaScript-ramverk.

The Shadow DOM

Medan webbkomponenten vi har byggt ovan fungerar är den inte immun mot yttre störningar, och CSS eller JavaScript kan ändra den. På samma sätt kan de stilar du definierar för din komponent läcka ut och påverka andra.

Shadow DOM löser detta inkapslingsproblem genom att fästa en separerad DOM till webbkomponenten med:

const shadow = this.attachShadow({ mode: 'closed' });

Läget kan antingen vara:

  1. “open” – JavaScript på yttersidan kan komma åt Shadow DOM (med shadowRoot), eller
  2. “closed” – Shadow DOM kan endast nås inom webbkomponenten.

Shadow DOM kan manipuleras som alla andra DOM-element:

connectedCallback() {

  const shadow = this.attachShadow({ mode: 'closed' });

  shadow.innerHTML = `
    <style>
      p {
        text-align: center;
        font-weight: normal;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>

    <p>Hello ${ this.name }!</p>`;

}

Komponenten renderar nu ”Hej”-texten inuti ett <p>-element och stylar det. Detta kan inte ändras av JavaScript eller CSS utanför komponenten, även om vissa stilar som typsnitt och färg ärvs från sidan eftersom de inte uttryckligen definierades.

CodePen demonstration

De formatmallar som hör till den här webbkomponenten kan inte påverka andra stycken på sidan eller ens andra <hello-world>-komponenter.

Observera att CSS :host-väljaren kan styla det yttre <hello-world>-elementet inifrån webbkomponenten:

:host {
  transform: rotate(180deg);
}

Du kan också ställa in stilar som ska tillämpas när elementet använder en viss klass, t.ex. <hello-world class="rotate90">:

:host(.rotate90) {
  transform: rotate(90deg);
}

HTML-mallar

Att definiera HTML inuti ett skript kan bli opraktiskt för mer komplexa webbkomponenter. Med en mall kan du definiera en bit HTML på din sida som din webbkomponent kan använda. Detta har flera fördelar:

  1. Du kan justera HTML-kod utan att behöva skriva om strängar inuti JavaScript.
  2. Komponenter kan anpassas utan att behöva skapa separata JavaScript-klasser för varje typ.
  3. Det är lättare att definiera HTML i HTML – och det kan ändras på servern eller klienten innan komponenten renderas.

Mallar definieras i en <template> tagg, och det är praktiskt att tilldela den ett ID så att du kan referera till den inom komponentklassen. Detta exempel har tre stycken för att visa meddelandet ”hello”:

<template id="hello-world">

  <style>
    p {
      text-align: center;
      font-weight: normal;
      padding: 0.5em;
      margin: 1px 0;
      background-color: #eee;
      border: 1px solid #666;
    }
  </style>

  <p class="hw-text"></p>
  <p class="hw-text"></p>
  <p class="hw-text"></p>

</template>

Webbkomponentklassen kan komma åt den här mallen, hämta dess innehåll och klona elementen för att säkerställa att du skapar ett unikt DOM-fragment överallt det används:

const template = document.getElementById('hello-world').content.cloneNode(true);

DOM kan ändras och läggas till direkt till Shadow DOM:

connectedCallback() {

  const

    shadow = this.attachShadow({ mode: 'closed' }),
    template = document.getElementById('hello-world').content.cloneNode(true),
    hwMsg = `Hello ${ this.name }`;

  Array.from( template.querySelectorAll('.hw-text') )
    .forEach( n => n.textContent = hwMsg );

  shadow.append( template );

}

CodePen demonstration

Mall-”slots”

Slots låter dig anpassa en mall. Anta att du ville använda din <hello-world>-komponent men placera meddelandet i en <h1>-rubrik i Shadow DOM. Då kan du skriva den här koden:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>

</hello-world>

(Notera slot-attributet.)

Du kan lägga till andra element som ett annat stycke:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Slots kan nu implementeras inom din mall:

<template id="hello-world">

  <slot name="msgtext" class="hw-text"></slot>

  <slot></slot>

</template>

Ett element-slot-attribut inställt på ”msgtext” ( <h1>) infogas vid den punkt där det finns en <slot> som heter ”msgtext”. <p> har inte ett slot-namn tilldelat sig men används i nästa tillgängliga namnlösa <slot>. I själva verket blir mallen:

<template id="hello-world">

  <slot name="msgtext" class="hw-text">
    <h1 slot="msgtext">Hello Default!</h1>
  </slot>

  <slot>
    <p>This text will become part of the component.</p>
  </slot>

</template>

Det är inte riktigt så enkelt i verkligheten. Ett <slot>-element i Shadow DOM pekar till de infogade elementen. Du kan bara komma åt dem genom att hitta en <slot> och sedan använda .assignedNodes()-metoden för att returnera en rad inre barnelement. Den uppdaterade connectedCallback()-metoden:

connectedCallback() {

  const
    shadow = this.attachShadow({ mode: 'closed' }),
    hwMsg = `Hello ${ this.name }`;

  // append shadow DOM
  shadow.append(
    document.getElementById('hello-world').content.cloneNode(true)
  );

  // find all slots with a hw-text class
  Array.from( shadow.querySelectorAll('slot.hw-text') )

    // update first assignedNode in slot
    .forEach( n => n.assignedNodes()[0].textContent = hwMsg );

}

CodePen demonstration

Dessutom kan du inte direkt formatera de infogade elementen, även om du kan inrikta dig på specifika slots i din webbkomponent:

<template id="hello-world">

  <style>
    slot[name="msgtext"] { color: green; }
  </style>

  <slot name="msgtext" class="hw-text"></slot>
  <slot></slot>

</template>

Mall-slots är lite ovanliga, men en fördel är att ditt innehåll kommer visas om JavaScript slutar köras. Den här koden visar en standardrubrik och ett stycke som endast ersätts när webbkomponentklassen framgångsrikt exekveras:

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Därför kan du implementera någon form av progressiv förbättring – även om det bara är ett ”Du behöver JavaScript”-meddelande!

Deklarativa Shadow DOM

Exemplen ovan konstruerar en Shadow DOM med JavaScript. Det är fortfarande det enda alternativet, men en experimentell deklarativ Shadow DOM utvecklas för Chrome. Detta tillåter rendering på serversidan och undviker alla layoutändringar eller stunder av ostylat innehåll.

Följande kod upptäcks av HTML-tolken, vilket skapar en identisk Shadow Dom till den du skapade i det sista avsnittet (du måste uppdatera meddelandet efter behov):

<hello-world name="Craig">

  <template shadowroot="closed">
    <slot name="msgtext" class="hw-text"></slot>
    <slot></slot>
  </template>

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>

</hello-world>

Funktionen är inte tillgänglig i någon webbläsare och det finns ingen garanti för att den kommer att nå Firefox eller Safari. Du kan läsa mer om deklarativ Shadow DOM, och en polyfill är lätt, men var medveten om att implementeringen kan förändras.

Shadow DOM-event

Din webbkomponent kan bifoga event till ett element i Shadow DOM precis som på sid-DOM, till exempel att lyssna efter klick-event på alla inre barn:

shadow.addEventListener('click', e => {

  // do something

});

Om du inte använder stopPropagation kommer eventet att bubbla upp i sid-DOM, men eventet kommer att riktas om. Därför verkar det komma från ditt anpassade element snarare än element i det.

Att använda webbkomponenter i andra ramverk

Alla webbkomponenter du skapar kommer att fungera i alla JavaScript-ramverk. Ingen av dem vet eller bryr sig om HTML-element – din <hello-world>-komponent kommer att behandlas på samma sätt som en <div> och placeras i DOM där klassen aktiveras.

custom-elements-everywhere.com ger en lista över ramverk och webbkomponent-anteckningar. De flesta är fullt kompatibla, men React.js har vissa utmaningar. Det är möjligt att använda <hello-world> i JSX:

import React from 'react';
import ReactDOM from 'react-dom';
import from './hello-world.js';

function MyPage() {

  return (
    <>
      <hello-world name="Craig"></hello-world> 
    </>
  );

}

ReactDOM.render(<MyPage />, document.getElementById('root'));

…men:

  • React kan endast skicka primitiva datatyper till HTML-attribut (inte arrayer eller objekt)
  • React kan inte lyssna efter webbkomponentevent, så du måste fästa dina egna hanterare manuellt.

Kritik och problem med webbkomponenter

Webbkomponenter har förbättrats avsevärt men vissa aspekter kan vara knepiga att hantera.

Stylingsvårigheter

Att styla webbkomponenter innebär vissa utmaningar, särskilt om du vill åsidosätta scoped-stilar. Det finns många lösningar:

  1. Undvik att använda Shadow DOM. Du kan lägga till innehåll direkt till ditt anpassade element, även om annan JavaScript oavsiktligt eller skadligt kan ändra det.
  2. Använd :host-klasser. Som vi såg ovan använder scoped CSS specifika stilar när en klass tillämpas på det anpassade elementet.
  3. Kolla in CSS anpassade egenskaper (variabler). Anpassade egenskaper sprider sig till webbkomponenter så om ditt element använder var (--my-color) kan du ställa in --my-color i en yttre behållare (t. ex. :root), och det kommer att användas.
  4. Utnyttja skuggdelar. Den nya ::part()selector kan styla en inre komponent som har ett part-attribut, dvs <h1 part="heading"> inuti en <hello-world> komponent kan formateras med väljaren hello-world::part(heading).
  5. Skicka in en sträng av stilar. Du kan skicka dem som ett attribut att tillämpa inom ett <style>-block.

Inget av detta är idealiskt och du måste planera hur andra användare ska kunna anpassa din webbkomponent noggrant.

Ignorerade inmatningar

Alla <input>, <textarea>, eller <select>-fält i din Shadow DOM associeras inte automatiskt i det innehållsformuläret. Tidiga webbkomponentanvändare la till dolda fält på sid-DOM eller använde FormData-gränssnittet för att uppdatera värden. Inget av dem är särskilt praktiskt och bryter webbkomponentens inkapsling.

Det nya ElementInternals-gränssnittet låter en webbkomponent kroka på formulär så att anpassade värden och giltighet kan definieras. Det är implementerat i Chrome, men en polyfill är tillgänglig för andra webbläsare.

Som demonstration ska vi skapa en grundläggande <input-age name="your-age"></input-age>-komponent. Klassen måste ha ett statiskt formAssociated-värde som sant och, eventuellt kan en formAssociatedCallback()-metod anropas när det yttre formuläret associeras:

// <input-age> web component
class InputAge extends HTMLElement {

  static formAssociated = true;

  formAssociatedCallback(form) {
    console.log('form associated:', form.id);
  }

Konstruktören måste nu köra attachInternals()-metoden, som gör det möjligt för komponenten att kommunicera med formuläret och annan JavaScript-kod som vill inspektera värdet eller valideringen:

  constructor() {

    super();
    this.internals = this.attachInternals();
    this.setValue('');

  }

  // set form value

  setValue(v) {

    this.value = v;

    this.internals.setFormValue(v);

  }

ElementInternals setFormValue()-metod anger elementets värde för det överordnade formuläret initierat med en tom sträng här (det kan också skickas ett FormData-objekt med flera namn/värdepar). Andra egenskaper och metoder inkluderar:

  • form: det överordnade formuläret
  • labels: en rad element som märker komponenten
  • Constraint Validation API-alternativ som willValidate, checkValidity, och validationMessage

connectedCallback()-metoden skapar en Shadow DOM precis som tidigare, men måste också övervaka fältet för förändringar, så setFormValue() kan köras:

  connectedCallback() {

    const shadow = this.attachShadow({ mode: 'closed' });

    shadow.innerHTML = `
      <style>input { width: 4em; }</style>
      <input type="number" placeholder="age" min="18" max="120" />`;

    // monitor input values
    shadow.querySelector('input').addEventListener('input', e => {
      this.setValue(e.target.value);
    });

  }

Du kan nu skapa ett HTML-formulär med hjälp av denna webbkomponent som fungerar på liknande sätt som andra formulärfält:

<form id="myform">

  <input type="text" name="your-name" placeholder="name" />

  <input-age name="your-age"></input-age>

  <button>submit</button>

</form>

Det fungerar, men det känns lite invecklat, det går inte att förneka.

Kolla in det i CodePen-demonstrationen

Mer information finns i denna artikel om mer kapabla formulärkontroller.

Sammanfattning

Webbkomponenter har kämpat för att bli accepterade på en marknad där JavaScript-ramverk har vuxit i storlek och kapacitet. Om du kommer från React, Vue.js, eller Angular kan webbkomponenter se komplexa och klumpiga ut, särskilt om du saknar funktioner som databindning och tillståndshantering.

Det finns absolut saker att fixa, men framtiden är ljus för webbkomponenter. De passar alla ramverk, är lätta, snabba och kan implementera funktionalitet som skulle vara omöjligt i endast JavaScript.

För tio år sedan skulle behöva tackla en webbplats utan jQuery, men webbläsarleverantörer tog de utmärkta delarna och lade till inbyggda alternativ (till exempel querySelector). Detsamma kommer att hända för JavaScript-ramverk, och webbkomponenter är det första trevande steget.

Har du några frågor om hur du använder Webbkomponenter? Låt oss prata om det i kommentarfältet nätet!

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.