Markup og oppførsel
Fristil er delt i to lag som aldri rører det samme. Det ene gir deg markupen, det andre gir deg oppførselen.
Markup eies av den som lager den. Oppførsel eies av komponenten.
Dette gjelder like mye om appen din rendrer alt i nettleseren, server-rendrer med hydrering, eller sender HTML fra serveren hele veien. Fristil er ikke et system for én av dem.
Virker dette i en klientrendret app?
Section titled “Virker dette i en klientrendret app?”Ja, og det er verdt å si tydelig, fordi delingen over lett leses som noe som bare angår servere.
Har du en React-app uten server i det hele tatt, lager React all markupen i nettleseren. Da kaller du byggefunksjonene der du rendrer, akkurat som du ville gjort på en server:
const felt = fs.field({ id: "epost", help: true, invalid })
<label {...felt.label}>E-postadresse</label><input {...fs.input({ type: "email" })} {...felt.control} /><p {...fs.helpText()} {...felt.help}>Vi sender kvittering hit.</p>Byggefunksjonene er rene funksjoner. De bryr seg ikke om hvor de kjører.
Det eneste som endrer seg mellom rendringsmodellene er hvor ofte markupen kan bli erstattet, og det er derfor komponentene ikke får lage den:
| Rendringsmodell | Markupen lages av | Kan erstatte den etterpå |
|---|---|---|
| Klientrendring (CSR) | React, i nettleseren | React, ved neste rendring |
| Server-rendring med hydrering (SSR) | Serveren, deretter React | React, ved neste rendring |
| Hypermedia (Datastar, htmx) | Serveren, også underveis | Svaret fra serveren |
I alle tre er det noe annet enn komponenten som bestemmer hva som står i DOM-en. Lager komponenten sitt eget innhold, blir det ryddet bort.
Med markup menes strukturen: elementene, klassene, rollene og koblingen
mellom dem. Den lager komponenten aldri. Tilstand som endrer seg mens
brukeren holder på, som aria-expanded på en åpen meny, setter komponenten,
og den setter den tilbake når en oppdatering fra serveren river den bort.
Skillet er altså ikke mellom attributt og node, men mellom det som beskriver
siden og det som beskriver hva brukeren nettopp gjorde.
Byggefunksjon eller web component?
Section titled “Byggefunksjon eller web component?”Dette er spørsmålet folk stopper på, og svaret er ulikt for én komponent enn for alle de andre.
<fs-field> er enten eller
Section titled “<fs-field> er enten eller”fs.field() og <fs-field> gjør nøyaktig den samme jobben: de kobler ledetekst, felt, hjelpetekst og feilmelding sammen. Du skal bruke den ene, ikke begge.
| Lager du markupen med JavaScript? | Bruk |
|---|---|
| Ja. React, Astro, en Node-server, eller en app helt uten server | fs.field(). Web componenten trengs ikke |
| Nei. En Go-mal, en PHP-fil, en Razor-visning, en publiseringsløsning eller håndskrevet HTML | <fs-field> |
fs.field() er en TypeScript-funksjon. Kan koden som lager HTML-en kalle den, er det den korteste veien: attributtene står ferdig i markupen, og nettleseren trenger ingen registrert komponent. Kan den ikke det, gjør <fs-field> det samme i nettleseren i stedet.
De andre er både og
Section titled “De andre er både og”De øvrige rammekomponentene gjør noe en funksjon umulig kan gjøre: de lytter på tastatur, flytter fokus og regner ut posisjon. Der bruker du begge deler.
const faner = fs.tabs({ id: "sak", count: 3 }) // markupen, fra byggefunksjonendefineFsTabs() // piltastene, fra komponenten| Komponent | Byggefunksjonen gir deg | Komponenten gir deg |
|---|---|---|
<fs-field> | koblingen | den samme koblingen, så velg én |
<fs-tabs> | roller, hidden, tabbestopp | piltaster, Home og End |
<fs-popover> | koblingen, popover-attributtet | posisjon, Escape, klikk utenfor |
<fs-suggestion> | feltet og lista | filtrering, piltaster, antall treff |
<fs-error-summary> | boksen, role="alert", overskriften | fokus hit, og videre til feltet |
<fs-dialog> | dialogen og koblingen til overskriften | showModal(), som en server ikke kan kalle |
Regelen bak tabellen: byggefunksjonen gir markup, komponenten gir oppførsel. <fs-field> er det eneste unntaket, og bare fordi koblingen er markup og ikke oppførsel.
De frittstående komponentene
Section titled “De frittstående komponentene”<fs-toast>, <fs-session-timeout> og <fs-connection-status> har også byggefunksjoner, men der gir de bare et tomt element med riktige attributter. Alt innholdet lager komponenten selv, fordi det ikke finnes før brukeren har gjort noe. Se Introduksjon.
Problemet det løser
Section titled “Problemet det løser”Et egendefinert element som lager sitt eget innhold i vanlig DOM havner i strid med rammeverket rundt. Rammeverket mener at HTML-en fra serveren er fasiten, og alt komponenten har lagt til er noe det ikke vet om.
Dette er ikke teoretisk. Vi har testet det, med ekte Datastar og en tidligere utgave av <fs-field> som satte koblingen selv. Serveren sendte det samme feltet på nytt én gang:
| Før serveren patchet | Etter | |
|---|---|---|
aria-describedby på feltet | fs-field-help-6vlv1ah | borte |
class på ledeteksten | fs-label | borte |
| Feilmeldingen skjult | ja | nei, den vises |
Den siste raden er den verste. En skjult feilmelding ble synlig, så brukeren fikk «E-posten mangler @» på et felt som var riktig utfylt. Og ingenting kom tilbake, for ingenting utløste en ny runde i komponenten.
Grunnen står i Datastars egen kildekode. Slik synkroniseres attributter under en morfing, der r er elementet i siden og s er det serveren sendte:
for (let {name: l} of Array.from(r.attributes)) !s.hasAttribute(l) && !o.includes(l) && r.removeAttribute(l)Et attributt som ikke står i serverens HTML blir fjernet. React har det samme problemet i en annen form: en strukturell rendring av foreldrekomponenten kan kaste bort noder et egendefinert element har lagt inn.
De to lagene
Section titled “De to lagene”| Lag | Hva det er | Hvor det kjører |
|---|---|---|
fs-byggefunksjonene | Klasser, data-*, tilgjengelighetskoblingen | Der du lager markupen, på server eller i nettleseren |
| Web-komponentene | Tastatur, fokus, posisjonering | I nettleseren, etter at HTML-en står der |
De overlapper ikke. En byggefunksjon setter aldri en hendelseslytter, og en komponent setter aldri et attributt på noe serveren eier.
En byggefunksjon er en ren funksjon. Du gir den et valgobjekt og får attributter tilbake:
import { fs } from "@fristil/designsystem"
fs.errorSummary({ count: 2, id: "feil" })// { container: { class: "fs-error-summary", role: "alert", tabindex: "-1", id: "feil" },// title: { class: "fs-error-summary__title" } }Det er hele grunnen til at det samme API-et virker i alle tre miljøene. Ingenting av dette krever en kjøretid.
De tre miljøene
Section titled “De tre miljøene”React Server Components. Byggefunksjonene er rene funksjoner uten klientkode. De kjører på serveren og forsvinner. Skal du bruke en web component i tillegg, kaller du defineFs* fra en klientkomponent.
TanStack Start. Spre attributtene i JSX. React eier dem og sender dem ut likt ved hver rendring, så det er ingenting som kan komme i utakt.
const feil = fs.errorSummary({ count: feilene.length, id: "feil" })
<fs-error-summary {...feil.container}> <h2 {...feil.title}>Du må rette {feilene.length} feil</h2></fs-error-summary>Datastar. Malen på serveren skriver de samme attributtene. Fordi de står i serverens utgave, finner morfingen ingenting å fjerne.
Datastar er det strengeste av de tre. Holder markupen der, holder den overalt.
Når komponenten må endre noe
Section titled “Når komponenten må endre noe”Noen attributter endrer seg mens brukeren holder på. aria-expanded på en knapp som åpner et panel, hidden på et fanepanel, posisjonen på et sprettoppvindu. Det er ekte oppførsel, og komponenten må få lov.
En oppdatering fra serveren river dem bort, siden ingenting av dem sto i HTML-en den sendte. Komponenten ser det og setter dem tilbake. Du trenger ikke gjøre noe, og malen din trenger ikke kjenne til attributtene i det hele tatt.
Skal serveren eie tilstanden, sier du det på verten:
<fs-tabs server-controlled>Da reparerer komponenten ingenting, og hver oppdatering bestemmer. Det er valget en server trenger når den skal kunne flytte fanen selv.
Når markupen ikke lages med JavaScript
Section titled “Når markupen ikke lages med JavaScript”fs.field() er en TypeScript-funksjon. Lager du markupen med JavaScript, kaller du den, og det spiller ingen rolle hvor koden kjører: React, Astro, en Node-server eller en app helt uten server.
Blir markupen til uten JavaScript, i en Go-mal, en PHP-fil, en Razor-visning eller håndskrevet HTML, finnes ikke den muligheten. Da gjør <fs-field> den samme koblingen i nettleseren i stedet. Det er alt som trengs på en side serveren sender én gang.
Når serveren sender det samme området på nytt
Section titled “Når serveren sender det samme området på nytt”Dette gjelder deg som bruker Datastar eller htmx, altså der serveren oppdaterer en del av siden mens brukeren holder på. Bruker du ikke det, kan du hoppe over resten av kapittelet.
Hva som skjer i en oppdatering
Section titled “Hva som skjer i en oppdatering”Datastar kan oppdatere på flere måter. Standarden, og den anbefalte, heter
outer, og den morfer: serveren sender ny HTML for et område, og Datastar
endrer elementene som allerede står der slik at de blir like den nye
versjonen. Elementene beholdes, bare det som er ulikt endres. Det er derfor
tekst du har skrevet i et felt ikke forsvinner.
(Det finnes også replace, som bytter ut elementet i stedet. Da mister du
alt nettleseren har lagt til, så den er ingen vei utenom det som står under.)
Men morfingen fjerner også alt den nye versjonen ikke har:
Serveren sendte: <button aria-selected="false">Vedlegg</button>Brukeren klikker, og <fs-tabs> setter:I siden står nå: <button aria-selected="true">Vedlegg</button>
Serveren oppdaterer området, og sender fortsatt: <button aria-selected="false">Vedlegg</button>
Fanevalget er borte.Serveren vet ikke hvilken fane brukeren valgte, så den sender den samme HTML-en som før, og morfingen setter siden tilbake.
Den enkleste løsningen: send bare det som har endret seg
Section titled “Den enkleste løsningen: send bare det som har endret seg”Datastar lar deg peke ut hva som skal oppdateres, med selector og
mode: inner. Har poengtavla endret seg, send tavla:
event: datastar-patch-elementsdata: selector #tavledata: mode innerdata: elements <tr><td>Kari</td><td>34</td></tr>Da røres ikke fanene eller skjemaet i det hele tatt, og ingenting kan gå tapt. Dette er den vanlige måten å bruke Datastar på, og den fjerner hele problemet.
Resten av kapittelet gjelder når du likevel må sende et større område på nytt.
Skriv struktur, og la komponenten koble
Section titled “Skriv struktur, og la komponenten koble”Med smale oppdateringer trenger serveren ikke skrive koblingen i det hele tatt. Dette er alt den sender:
<fs-field invalid required-marker="symbol"> <label>E-post</label> <input class="fs-input" type="email"> <p class="fs-help-text">Vi sender aldri spam.</p> <p class="fs-error-text">Skriv en gyldig adresse.</p></fs-field>Ingen id-er, ingen for, ingen aria-describedby. Klassene sier hvilket
element som er hva, og invalid sier at feltet er ugyldig. Resten gjør
komponenten, og dette står i siden etterpå:
label.class fs-labellabel.for fs-field-control-hzhptnylabel.data-required symbolinput.id fs-field-control-hzhptnyaria-describedby fs-field-help-1k0quej fs-field-error-dibhi29aria-invalid truedata-state invalidFeilmeldingen vises, og blir med i aria-describedby. Er feltet gyldig,
skjules den og tas ut av koblingen igjen.
Dette gjelder uansett hvilket språk serveren er skrevet i. En Go-mal, en PHP-fil eller en Ktor-app skriver det samme, og ingen av dem trenger å kjenne reglene. Det er hele poenget med at koblingen bor i komponenten.
Patchen river koblingen bort, siden ingenting av den sto i HTML-en serveren sendte. Komponenten ser at den er borte, og setter den tilbake. Id-ene blir de samme som før: komponenten husker dem, slik at en skjermleser midt i en opplesning ikke følger en peker som skifter under den.
Komponenten setter tilbake det brukeren gjorde
Section titled “Komponenten setter tilbake det brukeren gjorde”Noe kan serveren umulig vite: hvilken fane brukeren valgte, og om sprettoppvinduet er åpent. Det er brukerens tilstand, ikke serverens, og den lever bare i nettleseren. Ingenting av den står i HTML-en serveren sendte, så morfingen river den bort.
Komponentene setter den tilbake. Malen din trenger ikke vite hvilke attributter det gjelder:
| Komponent | Hva den setter tilbake |
|---|---|
<fs-field> | id, for, aria-describedby og resten av koblingen, med de samme id-ene som før |
<fs-tabs> | fanevalget: aria-selected og tabindex på fanene, hidden på panelene |
<fs-popover> | open på verten, aria-expanded på knappen, posisjonen på panelet |
<fs-suggestion> | om lista står åpen, hva som er markert, og hva filtreringen skjuler |
<fs-dialog> | open på selve <dialog>, som nettleseren setter i showModal() |
Dette erstatter data-preserve-attr, som krevde at malen skrev av navnene på
hvert attributt komponenten kom til å røre. Ni strenger å holde i takt med en
pakke som kunne endre seg, uten at noen kompilator så på dem.
Når serveren skal eie tilstanden
Section titled “Når serveren skal eie tilstanden”Fredningen ga én ting reparasjonen måtte erstatte: den som skrev markupen kunne bestemme hvem som eier tilstanden. «Gå videre til steg 2» er en ekte ting en server vil kunne gjøre.
Ett attributt gjør det, på verten:
<fs-tabs server-controlled>Da reparerer komponenten ingenting, og hver patch bestemmer. Standardvalget er det motsatte, fordi det er riktig nesten alltid: brukeren mister ikke det hun nettopp gjorde.
Sprettoppvinduet står i en mellomstilling, og det er med vilje. Har noen bedt
om at vinduet er åpent, blir det stående gjennom en patch. Sender serveren
open, åpnes det, for det er noe serveren faktisk sa.
En app lukker det med egenskapen: meny.open = false, hide() eller
toggle(). Styrer du open med et attributt utenfra, som med Datastars
data-attr:open, ser komponenten ingen forskjell på det og en morfing, og da
skal siden si server-controlled og la serveren eie tilstanden.
Når brukeren selv utløser oppdateringen
Section titled “Når brukeren selv utløser oppdateringen”Datastar sender signalene sine med hver forespørsel klienten starter. Lar du komponenten oppdatere et signal, vet serveren hva brukeren har gjort, og kan sende riktig HTML med én gang:
<fs-tabs server-controlled data-on:tab-select="$fane = evt.detail.index">Komponenten gjør tastaturet og tilgjengeligheten, signalet følger etter, og
serveren rendrer fra $fane. Da er serveren den som vet, og
server-controlled sier det. Uten attributtet ville de to kjempet om det
samme: komponenten setter tilbake fanen brukeren valgte, mens serveren sender
sin egen.
Lar du være å binde et signal, er det komponenten som vet, og da skal den reparere. Det er standardvalget, og det er også det riktige når serveren dytter en oppdatering av seg selv fordi en annen bruker gjorde noe.
Kort oppsummert
Section titled “Kort oppsummert”- Send bare det som har endret seg. Da oppstår ikke problemet.
- Skriv struktur, ikke kobling. Komponenten setter id-er,
forogaria-describedbyselv, i hvilket som helst språk. - Det brukeren gjorde setter komponenten tilbake. Du trenger ikke gjøre
noe. Skal serveren eie tilstanden, sier du det med
server-controlled.
Ett unntak finnes. Må du sende et stort område på nytt mens brukeren står og fyller det ut, kan serveren skrive hele koblingen selv. Da har komponenten ingenting å legge til, og morfingen ingenting å ta bort.
Regelen er de tre punktene over.
Komponenten sier fra når markupen ikke henger sammen
Section titled “Komponenten sier fra når markupen ikke henger sammen”En komponent fester oppførsel på markup noen andre har skrevet, og den markupen kan komme fra en Go-mal eller en publiseringsløsning der ingen kompilator ser etter. Fant komponenten ikke delene sine, gjorde den lenge ingenting, uten et ord, og feilen viste seg først når noen leste siden med skjermleser.
Nå kommer det en advarsel i konsollen med elementet som mangler noe:
fs-field: fant ingen kontroll å koble til. Ledeteksten, hjelpeteksten og feilmeldingen står uten et felt, og koblingen kan ikke lages. Sett inn et <input>, <textarea> eller <select>.Meldingen kommer én gang per element, ikke én gang per oppdatering, og bare når markupen faktisk er gal. Et tomt område serveren ikke har fylt ennå er ikke en feil.
Komponenter som eier sitt eget innhold
Section titled “Komponenter som eier sitt eget innhold”To komponenter kan ikke følge regelen, og de er beskrevet i arkitekturen:
<fs-session-timeout> teller ned mot at en innlogget økt går ut. Tallet kommer fra klientens klokke og endrer seg hvert sekund.
<fs-connection-status> sier fra når forbindelsen er borte. Serveren kan ikke melde det selv: er den utilgjengelig, kommer det ingenting derfra.
Begge setter data-ignore-morph, som ber Datastar la innholdet være. Byggefunksjonene skriver det ut, så du trenger ikke huske det.
Det som ikke finnes
Section titled “Det som ikke finnes”Fristil server-rendrer ikke web components. Det er ingen Lit SSR, ingen deklarativ shadow DOM, ingen byggesteg som krever Node.
Det er et valg. Lit SSR krever en JavaScript-server, og et designsystem som trenger Node for å få fram et datofelt er ikke lenger uavhengig av rammeverk. I stedet har vi fjernet behovet: markupen kommer fra serveren uansett, så det er ingenting å server-rendre.