Formulaires et liaisons
bind: relie un état à une propriété du DOM, dans les deux sens.
<script lang="ts">
let filtre = ''
</script>
<label for="f">Filtrer</label>
<input id="f" bind:value={filtre} />
<p>{filtre.length} caractères</p>
La cible doit être un let du composant. bind:value={objet.champ} est refusé :
écrire dans une propriété ne préviendrait pas le graphe réactif.
Champs
<input bind:value={texte} />
<input type="number" bind:value={nombre} />
<textarea bind:value={contenu}></textarea>
<select bind:value={choix}>
<option value="a">A</option>
</select>
<input type="checkbox" bind:checked={accepte} />
<input type="file" bind:files={fichiers} />
bind:value sur un <select> écoute change, sur les autres champs input : la
valeur suit la frappe.
Groupes
Plusieurs champs qui se partagent une valeur.
<script lang="ts">
let couleur = 'vert'
</script>
<label><input type="radio" bind:group={couleur} value="rouge" /> Rouge</label>
<label><input type="radio" bind:group={couleur} value="vert" /> Vert</label>
Pour des cases à cocher, l'état est le tableau des valeurs cochées :
<script lang="ts">
export let taches: Tache[]
let choisies: number[] = []
</script>
{#each taches as tache (tache.id)}
<label>
<input type="checkbox" bind:group={choisies} value={tache.id} />
{tache.titre}
</label>
{/each}
Un attribut HTML est toujours du texte. Quand la valeur écrite n'en est pas —
value={tache.id} est un nombre — elle est transmise à part, et le tableau contient
bien des nombres. Sans cela, il se remplirait de "1" au lieu de 1.
Le tableau est remplacé, jamais muté : la réactivité se fonde sur l'affectation.
Le nœud lui-même
<script lang="ts">
import { moveFocus } from '@ajustor/speedster-runtime'
let champ: HTMLInputElement | null = null
const cibler = () => { if (champ !== null) moveFocus(champ) }
</script>
<input bind:this={champ} aria-label="Recherche" />
<button on:click={cibler}>Aller au champ</button>
La référence est rendue à null quand le nœud disparaît — un bloc qui se ferme,
un composant qui se démonte. Sans cela, le composant garderait un nœud détaché,
invisible mais bien vivant.
Déclarez donc le type comme … | null.
Dimensions
<script lang="ts">
let largeur = 0
</script>
<div bind:clientWidth={largeur}>
{#if largeur < 600}<Compact />{:else}<Etendu />{/if}
</div>
clientWidth, clientHeight, offsetWidth, offsetHeight. La liaison va dans un
seul sens : une dimension se lit, elle ne s'écrit pas.
Un ResizeObserver unique est partagé par tous les éléments mesurés de
l'application — pas un par élément.
Média
<script lang="ts">
let position = 0
let enPause = true
let duree = 0
let volume = 1
</script>
<video
src="/film.mp4"
bind:currentTime={position}
bind:paused={enPause}
bind:duration={duree}
bind:volume={volume}
>
<track kind="captions" src="/film.vtt" />
</video>
<p>{Math.round(position)} s sur {Math.round(duree)} s</p>
<button on:click={() => { enPause = !enPause }}>
{enPause ? 'Lire' : 'Pause'}
</button>
| Liaison | Sens | Événement écouté |
|---|---|---|
currentTime |
↔ | timeupdate |
paused |
↔ | play, pause |
volume, muted |
↔ | volumechange |
playbackRate |
↔ | ratechange |
duration |
→ lecture seule | durationchange |
ended |
→ lecture seule | ended, play, emptied |
seeking |
→ lecture seule | seeking, seeked |
videoWidth, videoHeight |
→ lecture seule | loadedmetadata |
Une propriété que seul le navigateur décide n'est jamais réécrite : lui écrire une valeur ferait taire le navigateur.
Sur une image : bind:naturalWidth, bind:naturalHeight (lecture seule, load).
Autres
<details bind:open={ouvert}>
<summary>Détails</summary>
<p>…</p>
</details>
<div contenteditable="true" role="textbox" aria-label="Notes" bind:innerHTML={notes}></div>
open écoute toggle — un événement qui ne remonte pas l'arbre, et qui passe donc
par un écouteur direct. innerHTML, innerText et textContent écoutent input.
bind:indeterminate écrit la propriété sans jamais la relire : c'est un état que le
navigateur ne modifie pas de lui-même.
Ce qui n'est pas là
bind:scrollY, bind:innerWidth et les autres liaisons portant sur la fenêtre :
elles supposeraient un équivalent de <svelte:window>, qui n'existe pas. En
attendant, un effet suffit :
<script lang="ts">
import { effect } from '@ajustor/speedster-runtime'
let defilement = 0
effect(() => {
const suivre = () => { defilement = window.scrollY }
window.addEventListener('scroll', suivre, { passive: true })
return () => window.removeEventListener('scroll', suivre)
})
</script>
Un formulaire complet
<script lang="ts">
interface Brouillon { titre: string; urgent: boolean; categorie: string }
export let enregistrer: (valeur: Brouillon) => void
let titre = ''
let urgent = false
let categorie = 'general'
const valide = titre.trim().length > 0
const envoyer = () => {
if (!valide) return
enregistrer({ titre, urgent, categorie })
titre = ''
urgent = false
}
</script>
<form on:submit|preventDefault={envoyer}>
<label for="titre">Titre</label>
<input id="titre" bind:value={titre} required />
<label>
<input type="checkbox" bind:checked={urgent} />
Urgent
</label>
<label for="categorie">Catégorie</label>
<select id="categorie" bind:value={categorie}>
<option value="general">Général</option>
<option value="travail">Travail</option>
</select>
<button type="submit" disabled={!valide}>Ajouter</button>
</form>
valide est une dérivée : elle se recalcule quand titre change, et le bouton
suit. Rien à déclarer.
Les trois <label> sont associés à leur champ — le compilateur le vérifie, et
signale a11y-missing-name si un champ n'a rien qui le nomme. Voir
Accessibilité.