Gå til innholdet

Typesikker bruk

Klasser og dataattributter gjør komponentene rammeverksuavhengige, men alt er strenger:

<button class="fs-button" data-variant="secundary">Lagre utkast</button>

Knappen får primærstilen. Ingen feilmelding, bare en variant som stille aldri ble brukt. Derfor finnes fs.

import { fs } from "@fristil/designsystem"
<button {...fs.button({ variant: "secondary" })}>Lagre utkast</button>
<input {...fs.input({ type: "email", state: "invalid" })} />
<span {...fs.badge({ color: "success" })}>Innvilget</span>
<a {...fs.link()} href="/kvittering">Se kvittering</a>

Skriv fs. i editoren, så ser du hele systemet. Hver komponent er en funksjon som tar et valgobjekt og returnerer attributtene du sprer inn i elementet. Formen er den samme for alle.

Returverdiene er vanlige objekter. Derfor virker samme API i React, Astro, Datastar og ren HTML.

I JSX sprer du objektet rett inn i elementet. Uten JSX gjør fs.setAttributes jobben:

const felt = document.querySelector<HTMLInputElement>("#epost")
fs.setAttributes(felt, fs.input({ type: "email", state: "invalid" }))

Kall den på nytt for å endre tilstand. Attributter fra forrige kall som ikke er med i det nye settet blir fjernet. Ellers ville feltet blitt stående rødt etter at feilen var rettet. Den rydder bare i systemets egne attributter og fs--klasser; data-testid, name og dine egne klasser står urørt.

fs.button()
// { class: "fs-button" }
fs.button({ variant: "primary" })
// { class: "fs-button" }, primær er standard i CSS-en
fs.button({ variant: "danger" })
// { class: "fs-button", "data-variant": "danger" }

Du sier alltid hvilken variant du vil ha; funksjonen vet når attributtet kan utelates.

fs.button.variants
// readonly ["primary", "secondary", "ghost", "danger"]
fs.button.isVariant("ghost") // true
fs.button.isVariant("gost") // false

Samme mønster overalt: fs.badge.colors og fs.badge.isColor, fs.input.types og fs.input.isType.

Kommer verdien fra et API, en URL eller localStorage, er den en string TypeScript ikke kan garantere noe om. Vakten gir en gyldig verdi eller en trygg reserve:

const svar = await hentKnappeoppsett()
const variant = fs.button.isVariant(svar.variant) ? svar.variant : "primary"
fs.button({ variant })

Etter vakten vet TypeScript at verdien er en ButtonVariant.

Skrevet for hånd må type og data-variant stemme overens, ellers skjer ingenting når brukeren trykker på ikonet.

fs.input({ type: "date" })
// { class: "fs-input", type: "date", "data-variant": "date" }
fs.input({ type: "email" })
// { class: "fs-input", type: "email" }, e-post har ikke noe ikon

Én verdi styrer begge. Det samme gjelder tilstand: state: "invalid" gir både data-state og aria-invalid.

ValgHva det betyrHvor
variantVisuell vekt eller betydningbutton, helpText, errorText
colorHva en status betyrbadge
stateValideringstilstandinput, textarea, select
typeHTML-typen på feltetinput

state er én type på tvers av de tre feltkomponentene, så en verdi kan sendes mellom dem uten konvertering:

const tilstand = fs.isState(fraServer) ? fraServer : "default"
fs.input({ state: tilstand })
fs.textarea({ state: tilstand })
fs.select({ state: tilstand })

Fem koblinger må stemme samtidig. fs.field regner dem ut:

const felt = fs.field({
id: "epost",
required: "symbol",
help: true,
error: true,
invalid: !erGyldig,
})
<label {...felt.label}>E-postadresse</label>
<input {...fs.input({ type: "email" })} {...felt.control} />
<p {...fs.helpText()} {...felt.help}>Vi sender kvittering hit.</p>
<p {...fs.errorText()} {...felt.error}>Skriv en gyldig adresse.</p>

Uten JSX:

fs.setAttributes(ledetekst, felt.label)
fs.setAttributes(input, { ...fs.input({ type: "email" }), ...felt.control })
fs.setAttributes(hjelpetekst, { ...fs.helpText(), ...felt.help })
fs.setAttributes(feilmelding, { ...fs.errorText(), ...felt.error })

Du får for, id, aria-describedby, aria-invalid og data-state ferdig utregnet.

Legg merke til at fs.input bare får type. Tilstanden ligger i felt.control, og de to overlapper med vilje ikke. Sendte du state til begge, ville aria-invalid blitt skrevet ut to ganger i maler som gjengir attributtene bokstavelig, som Astro og ren HTML. felt.state finnes fortsatt hvis du vil drive noe annet med den.

Feilmeldingen tas med i aria-describedby bare når feltet er ugyldig. Ellers ville skjermleseren pekt på et skjult element. felt.error inneholder hidden når den ikke skal vises.

import { fs } from "@fristil/designsystem/react"
import type { ButtonVariant } from "@fristil/designsystem"
import type { ComponentProps } from "react"
type Props = ComponentProps<"button"> & {
variant?: ButtonVariant
}
export function Knapp({ variant, ...rest }: Props) {
return <button {...fs.button({ variant })} {...rest} />
}

Bytter teamet rammeverk, er det disse tolv linjene som skrives om, ikke designsystemet.

ComponentProps<"button"> tar med ref, som siden React 19 er en vanlig prop og følger med gjennom {...rest} uten forwardRef. ComponentPropsWithRef og ComponentPropsWithoutRef trengs ikke lenger.

TypeScript kjenner ikke <fs-field>, <fs-session-timeout> og <fs-connection-status> uten videre. Importer typene én gang i en .d.ts-fil:

src/fristil.d.ts
import "@fristil/designsystem/react-jsx"

Da får attributtene autofullføring og sjekk:

<fs-field required-marker="text" /> // ok
<fs-field required-marker="tekst" /> // feil: "none" | "symbol" | "text"
<fs-session-timeout warnAt={1500} /> // feil: attributtet heter warn-at
<fs-connection-status offline="Nede" /> // feil: ukjent attributt

Ren HTML, Astro og Datastar trenger ikke dette. Der er <fs-field> bare en HTML-tagg.

Attributtene kan spres på feil element. <div {...fs.button({ variant: "danger" })} /> kompilerer, for returverdien er bare et objekt med HTML-attributter.

Returtypen på type er bredere enn verdien du sendte inn. fs.input({ type: "date" }).type er hele unionen, ikke "date".

fs er valgfritt. Uten TypeScript eller byggesteg er dette like gyldig:

<button class="fs-button" data-variant="secondary">Lagre utkast</button>

Resultatet er identisk. Forskjellen er hvor tidlig du får vite at du skrev secundary.

Vil du bare ha én komponent:

import { button } from "@fristil/designsystem/button"

Subpath-ene returnerer class og for, som hovedinngangen. React-varianten finnes bare som samlet fs.