Hoppa till innehåll

En komponents filer

En komponent ska gå att flytta. Kopierar du dess katalog till ett annat projekt ska den fungera där också, utan att du behöver leta upp bilder, stilar eller registreringskod på andra ställen. Den här artikeln visar hur kursens komponenter är organiserade för att det ska vara möjligt. Det är samma struktur som i övningsuppgifterna och som examinationsuppgift B2 kräver.

Tidsåtgång: Ungefär 12 minuter.

Varje komponent har en egen katalog under src/js/components/, med samma namn som elementet. Så här ser projektet i övningsuppgiften Tiny Tunes ut när tiny-time är klar:

  • Mappsrc
    • Mappcss
      • styles.css
    • Mappjs
      • Mappcomponents
        • Mapptiny-time
          • index.js registrerar elementet
          • README.md dokumenterar komponenten
          • tiny-time.css stilarna i shadow DOM
          • tiny-time.html uppmärkningen i shadow DOM
          • tiny-time.js klassen
        • Mapptiny-tune
          • …
      • index.js applikationens startpunkt
    • index.html

Varje fil har ett enda ansvar. HTML skrivs i en .html-fil och CSS i en .css-fil, så att editorn, lintern och formateraren behandlar dem som HTML och CSS. Klassen ligger i en fil med komponentens namn, och index.js gör bara en sak: registrerar elementet.

Överst i tiny-time.js importeras uppmärkningen och stilarna:

import htmlText from './tiny-time.html?raw'
import cssText from './tiny-time.css?inline'
const template = document.createElement('template')
template.innerHTML = htmlText
const stylesheet = new CSSStyleSheet()
stylesheet.replaceSync(cssText)

?raw och ?inline är inte en del av JavaScript. Det är tillägg som Vite förstår: de säger åt Vite att ge dig filens innehåll som en vanlig sträng. Vite skriver om importerna innan koden når webbläsaren, så det blir inga extra förfrågningar, och i produktionsbygget ligger strängarna direkt i den paketerade koden.

Koden fungerar alltså bara tillsammans med Vite. Det är ett medvetet val, eftersom alla kursens projekt redan använder Vite. Alternativen du kan stöta på står sig så här:

  • CSS-moduler, import stylesheet from './x.css' with { type: 'css' }, är standard och ger direkt ett CSSStyleSheet. Vite kan i dagsläget inte bygga dem, men det kan ändras.
  • HTML-moduler, with { type: 'html' }, finns inte. Idén har utforskats men lagts ner.
  • fetch() av filerna fungerar utan Vite, men komponenten kan inte registreras förrän filerna har hämtats. Under tiden står elementet odefinierat på sidan, och varje sidladdning kostar extra förfrågningar.

Övningsuppgiften bart-board i kursvecka 11 har en fil, ARCHITECTURE.md, som går igenom valet mer utförligt.

I introduktionen i kursvecka 8 skickades en klass utan namn direkt till customElements.define(). För ett första exempel räcker det. I kursens komponenter är klassen i stället namngiven och exporterad från en egen fil, och registreringen sker i index.js:

tiny-time.js
export class TinyTime extends HTMLElement {
// ...
}
index.js
import { TinyTime } from './tiny-time.js'
if (!customElements.get('tiny-time')) {
customElements.define('tiny-time', TinyTime)
}

Skälet är regeln från första artikeln: ett namn och en klass kan bara registreras en gång per sida. Om tiny-time.js själv anropade define() skulle varje import av klassen också registrera den. Med uppdelningen kan klassen importeras utan att något registreras, till exempel av en testfil som registrerar den under ett eget, tillfälligt namn. Så är testerna i bart-board skrivna.

Kontrollen med customElements.get() skyddar mot att index.js körs mer än en gång, vilket kan hända om samma fil importeras via två olika sökvägar som paketeraren inte känner igen som samma fil.

Beroendediagram. js/index.js använder tiny-time/index.js via import './components/tiny-time/index.js'. tiny-time/index.js använder tiny-time.js via import { TinyTime }. tiny-time.js använder tiny-time.html via ?raw och tiny-time.css via ?inline. En anteckning vid tiny-time/index.js säger att det är det enda stället där customElements.define anropas.

Applikationen laddar komponenten med en import utan namn:

src/js/index.js
import './components/tiny-tune/index.js'
import './components/tiny-time/index.js'

En sådan import körs bara för sin sidoeffekt: modulen körs, och define() anropas. Inget behöver importeras med namn. Eftersom en modul körs först när alla dess importer har körts är komponenterna garanterat registrerade när resten av index.js körs, oavsett i vilken ordning skripten står i HTML- koden.

Skriv ut hela sökvägen, med index.js och filändelsen. Du kan stöta på kod som bara pekar på katalogen, import './components/tiny-time/'. Det fungerar i Vite, men är ett arv från CommonJS i Node.js. Varken webbläsaren eller ES-moduler i Node.js letar upp index.js i en katalog, och en sökväg som bara fungerar tack vare paketeraren döljer vilken fil som faktiskt laddas.

Du har skrivit klart tiny-time och lagt till den i index.html:

<div id="step09">
<tiny-time></tiny-time>
</div>

Men du har glömt raden import './components/tiny-time/index.js' i src/js/index.js. Vad ser du på sidan, och vad står i konsolen?

Ingenting syns, och konsolen är tom.

Ingen kod har importerat tiny-time/index.js, så den körs aldrig och define() anropas aldrig. <tiny-time> är ett giltigt namn för ett custom element, så webbläsaren skapar ett odefinierat element utan innehåll och väntar på en registrering som aldrig kommer. Det är inget fel enligt webbläsaren, så inget felmeddelande visas.

Ser ditt element tomt ut, kontrollera först i utvecklarverktygen om det har en #shadow-root. Saknas den har komponenten inte registrerats.

En komponent som visar en standardbild behöver en sökväg till bilden. En sökväg som /images/default.png utgår från webbplatsens rot och slutar fungera så fort komponenten flyttas till ett projekt där bilden inte ligger på samma plats. Bilden hör till komponenten och ska ligga i dess katalog, med en sökväg som utgår från komponentens egen fil.

Det standardiserade sättet är import.meta.url, som ger den aktuella modulens egen URL, tillsammans med URL-klassen:

src/js/components/user-card/user-card.js
const defaultAvatar = new URL('./images/default.png', import.meta.url)
// http://localhost:5173/js/components/user-card/images/default.png

new URL() räknar ut en relativ sökväg på samma sätt som webbläsaren gör med en länk. Vite känner igen mönstret och tar med bilden i produktionsbygget, så länge sökvägen är en fast sträng. I Vite kan du också importera bilden direkt, import defaultAvatar from './images/default.png', och få en URL tillbaka. Båda sätten gör komponenten flyttbar.

Bilder som hör till sidan i stort, som bilderna i steg 5 i Tiny Tunes, ligger i stället i public/ och nås med en sökväg som börjar med /.

Modulen ligger på http://localhost:5173/js/components/user-card/user-card.js. Vad ger

new URL('../shared/icon.svg', import.meta.url).href

http://localhost:5173/js/components/shared/icon.svg

Den relativa sökvägen räknas från katalogen som modulen ligger i, .../components/user-card/. .. går upp till .../components/, och därifrån till shared/icon.svg. Det är samma regler som för en relativ länk i HTML.

Lägg också märke till att en komponent som pekar ut ur sin egen katalog inte längre är helt flyttbar.

En komponent kan använda npm-paket som all annan kod. tiny-tune spelar sina toner med paketet Tone.js:

import * as Tone from 'tone'

Paketet installeras med npm install och står i projektets package.json. Det kopieras inte in i komponentens katalog. Vite hittar det i node_modules och paketerar det tillsammans med resten av koden. Vill du flytta komponenten till ett annat projekt behöver du alltså även lägga till beroendet där, och därför ska komponentens dokumentation nämna det.

En komponent används av andra än den som skrev den, och därför är dess README.md en del av komponenten. Den beskriver komponentens publika gränssnitt, allt som syns utifrån. Den skrivs, precis som koden, på engelska: dokumentationen följer med komponenten dit den flyttas, och är en förlängning av koden snarare än en text om den.

# user-card
Displays the name and description of a user.
## Attributes
### `name`
The user's name. Default: `Unknown`.
## Methods
None.
## Events
None.
## Styling with CSS
| Part or custom property | Description |
| --- | --- |
| `::part(name)` | The heading containing the name. |
| `--user-card-name-color` | The color of the name. |
## Example
```html
<user-card name="Ada Lovelace">
<p>Wrote the first published computer program.</p>
</user-card>
```

bart-board har en README.md med samma uppbyggnad, och det har även tiny-time i lösningen till Tiny Tunes. I examinationsuppgift B2 är en README.md per komponent ett krav.

Några tumregler som gör en komponent lätt att använda:

  • Bete dig som de inbyggda elementen. Kalla ett attribut för en bildadress src, inte source. Ge elementet display: block och respektera hidden.
  • Ha rimliga standardvärden. En komponent ska fungera utan att varje attribut är satt. Det är bättre att den visar Unknown än att den kastar ett fel.
  • Låt användaren bestämma över elementet självt. Komponenten ändrar i sitt eget shadow-träd, men lägger inte till klasser eller attribut på sig själv som användaren inte bett om. Behöver elementet en roll för skärmläsare, till exempel role="timer", sätts den med ElementInternals: this.attachInternals().role = 'timer' i konstruktorn, i stället för med setAttribute().
  • Data ner, händelser upp. Komponenten får data via attribut och metoder, och meddelar omvärlden med egna händelser. Det är ämnet för kursvecka 11.
  • Samla allt i katalogen. HTML, CSS, bilder och dokumentation ligger hos komponenten. Bara npm-paket ligger utanför.
  • En komponent, en katalog under src/js/components/, med <namn>.html, <namn>.css, <namn>.js, index.js och README.md.
  • ?raw och ?inline är Vite-tillägg som importerar filernas innehåll som strängar.
  • Klassen exporteras från <namn>.js. Bara index.js anropar customElements.define(), och bara om namnet inte redan är registrerat.
  • Applikationen laddar komponenten med import './components/<namn>/index.js', med hela sökvägen.
  • Komponentens egna resurser nås relativt modulen, med new URL(..., import.meta.url) eller en import.
CCBY