Binding
Cos’è il Two-Way Binding
In Svelte, il binding bidirezionale (bind:) permette di sincronizzare automaticamente una variabile con un elemento del DOM. Quando l’utente modifica l’input, la variabile si aggiorna; quando la variabile cambia nel codice, l’input si aggiorna di conseguenza.
<script>
let nome = $state('');
</script>
<!-- Two-way binding: nome e input sono sempre sincronizzati -->
<input bind:value={nome} />
<p>Ciao, {nome || 'sconosciuto'}!</p>
Senza bind:, dovresti gestire manualmente l’evento input:
<!-- Senza bind (one-way + handler manuale) -->
<input value={nome} oninput={(e) => nome = e.target.value} />
<!-- Con bind (molto più conciso) -->
<input bind:value={nome} />
bind:value per Input di Testo
Input text
<script>
let nome = $state('');
let email = $state('');
let password = $state('');
</script>
<input type="text" bind:value={nome} placeholder="Nome" />
<input type="email" bind:value={email} placeholder="Email" />
<input type="password" bind:value={password} placeholder="Password" />
<p>Nome: {nome}</p>
<p>Email: {email}</p>
Textarea
<script>
let messaggio = $state('');
</script>
<textarea bind:value={messaggio} rows="5" placeholder="Scrivi qui..."></textarea>
<p>Caratteri: {messaggio.length}</p>
Input numerici
Con type="number" e type="range", il valore viene automaticamente convertito in numero:
<script>
let quantita = $state(1);
let volume = $state(50);
</script>
<!-- bind:value restituisce un number, non una stringa -->
<input type="number" bind:value={quantita} min="0" max="100" />
<input type="range" bind:value={volume} min="0" max="100" />
<p>Quantità: {quantita} (tipo: {typeof quantita})</p>
<p>Volume: {volume}%</p>
Select
<script>
let linguaggio = $state('');
let linguaggi = ['JavaScript', 'TypeScript', 'Python', 'Rust', 'Go'];
</script>
<select bind:value={linguaggio}>
<option value="">-- Seleziona --</option>
{#each linguaggi as lang}
<option value={lang}>{lang}</option>
{/each}
</select>
<p>Hai scelto: {linguaggio || 'nessuno'}</p>
Select multiplo
<script>
let selezionati = $state([]);
let opzioni = ['Calcio', 'Tennis', 'Basket', 'Nuoto', 'Ciclismo'];
</script>
<!-- Con multiple, il valore è un array -->
<select bind:value={selezionati} multiple>
{#each opzioni as sport}
<option value={sport}>{sport}</option>
{/each}
</select>
<p>Sport selezionati: {selezionati.join(', ') || 'nessuno'}</p>
bind:checked per Checkbox
<script>
let accetto = $state(false);
let notifiche = $state(true);
let newsletter = $state(false);
</script>
<label>
<input type="checkbox" bind:checked={accetto} />
Accetto i termini e condizioni
</label>
<label>
<input type="checkbox" bind:checked={notifiche} />
Attiva notifiche
</label>
<label>
<input type="checkbox" bind:checked={newsletter} />
Iscriviti alla newsletter
</label>
<button disabled={!accetto}>Invia</button>
<p>Notifiche: {notifiche ? 'Attive' : 'Disattive'}</p>
bind:group per Radio e Checkbox Raggruppati
Radio buttons
bind:group collega più input radio alla stessa variabile:
<script>
let colore = $state('rosso');
</script>
<fieldset>
<legend>Scegli un colore:</legend>
<label>
<input type="radio" bind:group={colore} value="rosso" />
Rosso
</label>
<label>
<input type="radio" bind:group={colore} value="verde" />
Verde
</label>
<label>
<input type="radio" bind:group={colore} value="blu" />
Blu
</label>
</fieldset>
<p>Colore selezionato: {colore}</p>
<div style="width: 50px; height: 50px; background: {colore}"></div>
Checkbox raggruppate
Con le checkbox, bind:group gestisce un array di valori selezionati:
<script>
let ingredienti = $state([]);
let opzioni = ['Mozzarella', 'Pomodoro', 'Basilico', 'Prosciutto', 'Funghi', 'Olive'];
</script>
<fieldset>
<legend>Scegli gli ingredienti:</legend>
{#each opzioni as ingrediente}
<label>
<input type="checkbox" bind:group={ingredienti} value={ingrediente} />
{ingrediente}
</label>
{/each}
</fieldset>
<p>Selezionati ({ingredienti.length}): {ingredienti.join(', ') || 'nessuno'}</p>
bind:this per Riferimento al DOM
bind:this salva un riferimento diretto all’elemento DOM in una variabile:
<script>
let inputElement;
let canvasElement;
function focusSuInput() {
inputElement.focus();
}
$effect(() => {
if (canvasElement) {
const ctx = canvasElement.getContext('2d');
ctx.fillStyle = '#ff3e00';
ctx.fillRect(10, 10, 100, 50);
}
});
</script>
<input bind:this={inputElement} placeholder="Clicca il bottone per il focus" />
<button onclick={focusSuInput}>Focus sull'input</button>
<canvas bind:this={canvasElement} width="200" height="100"></canvas>
bind:this con componenti
bind:this su un componente restituisce l’istanza del componente, permettendo di accedere alle proprietà e metodi esportati:
<!-- Figlio.svelte -->
<script>
export function saluta() {
alert('Ciao dal componente figlio!');
}
export function reset() {
// logica di reset
}
</script>
<!-- Genitore.svelte -->
<script>
import Figlio from './Figlio.svelte';
let figlioRef;
</script>
<Figlio bind:this={figlioRef} />
<button onclick={() => figlioRef.saluta()}>Chiama metodo del figlio</button>
Binding per Dimensioni
Svelte permette di leggere (ma non scrivere) le dimensioni degli elementi:
<script>
let larghezza = $state(0);
let altezza = $state(0);
let offsetWidth = $state(0);
let offsetHeight = $state(0);
</script>
<div bind:clientWidth={larghezza} bind:clientHeight={altezza}>
<p>Questo div è largo {larghezza}px e alto {altezza}px</p>
<p>Ridimensiona la finestra per vedere i valori cambiare.</p>
</div>
<!-- Anche offsetWidth e offsetHeight sono disponibili -->
<div bind:offsetWidth={offsetWidth} bind:offsetHeight={offsetHeight}>
Offset: {offsetWidth} x {offsetHeight}
</div>
<style>
div {
border: 2px solid #ccc;
padding: 1rem;
resize: both;
overflow: auto;
}
</style>
I binding dimensionali disponibili sono:
bind:clientWidth/bind:clientHeight- dimensioni interne (padding incluso)bind:offsetWidth/bind:offsetHeight- dimensioni esterne (border incluso)
Binding per Elementi Media
Audio e Video
<script>
let currentTime = $state(0);
let duration = $state(0);
let paused = $state(true);
let volume = $state(1);
let muted = $state(false);
let formattato = $derived(
`${Math.floor(currentTime / 60)}:${String(Math.floor(currentTime % 60)).padStart(2, '0')}`
);
</script>
<video
bind:currentTime
bind:duration
bind:paused
bind:volume
bind:muted
src="/video/demo.mp4"
width="640"
>
<track kind="captions" />
</video>
<div class="controlli">
<button onclick={() => paused = !paused}>
{paused ? 'Play' : 'Pausa'}
</button>
<span>{formattato} / {Math.floor(duration / 60)}:{String(Math.floor(duration % 60)).padStart(2, '0')}</span>
<input type="range" bind:value={currentTime} min="0" max={duration} step="0.1" />
<label>
Volume:
<input type="range" bind:value={volume} min="0" max="1" step="0.01" />
</label>
<label>
<input type="checkbox" bind:checked={muted} />
Muto
</label>
</div>
I binding media disponibili sono:
bind:currentTime(lettura/scrittura)bind:duration(solo lettura)bind:paused(lettura/scrittura)bind:volume(lettura/scrittura)bind:muted(lettura/scrittura)bind:playbackRate(lettura/scrittura)bind:seeking(solo lettura)bind:ended(solo lettura)bind:buffered(solo lettura)
Binding per Contenteditable
<script>
let html = $state('<b>Testo</b> <i>editabile</i>');
let testo = $state('Solo testo');
</script>
<!-- bind:innerHTML per contenuto HTML -->
<div contenteditable="true" bind:innerHTML={html}></div>
<p>HTML: {html}</p>
<!-- bind:textContent per solo testo -->
<div contenteditable="true" bind:textContent={testo}></div>
<p>Testo: {testo}</p>
<style>
div[contenteditable] {
border: 1px solid #ccc;
padding: 0.5rem;
min-height: 2rem;
}
</style>
Binding con Oggetti Select
I <select> in Svelte possono fare binding a valori non-stringa:
<script>
let persone = $state([
{ id: 1, nome: 'Mario', eta: 30 },
{ id: 2, nome: 'Lucia', eta: 25 },
{ id: 3, nome: 'Giovanni', eta: 35 }
]);
let selezionato = $state(persone[0]);
</script>
<select bind:value={selezionato}>
{#each persone as persona}
<!-- Il valore è l'intero oggetto, non solo una stringa -->
<option value={persona}>{persona.nome}</option>
{/each}
</select>
{#if selezionato}
<p>{selezionato.nome} ha {selezionato.eta} anni (ID: {selezionato.id})</p>
{/if}
Esempio Pratico: Form Completo
<script>
let form = $state({
nome: '',
email: '',
tipo: 'personale',
interessi: [],
bio: '',
accetto: false
});
let sommario = $derived.by(() => {
return `${form.nome} (${form.email}) - ${form.tipo} - ${form.interessi.length} interessi`;
});
function invia() {
console.log('Dati form:', $state.snapshot(form));
}
</script>
<form onsubmit={(e) => { e.preventDefault(); invia(); }}>
<input bind:value={form.nome} placeholder="Nome" required />
<input type="email" bind:value={form.email} placeholder="Email" required />
<select bind:value={form.tipo}>
<option value="personale">Personale</option>
<option value="lavoro">Lavoro</option>
</select>
{#each ['Sport', 'Musica', 'Cinema', 'Viaggi'] as interesse}
<label>
<input type="checkbox" bind:group={form.interessi} value={interesse} />
{interesse}
</label>
{/each}
<textarea bind:value={form.bio} placeholder="Biografia"></textarea>
<label>
<input type="checkbox" bind:checked={form.accetto} />
Accetto i termini
</label>
<button type="submit" disabled={!form.accetto}>Invia</button>
</form>
<p>{sommario}</p>
Best Practice
- Usa
bind:valueper input di testo, select e textarea – è più conciso di gestire gli eventi manualmente - Preferisci il one-way data flow quando possibile: il binding bidirezionale può rendere il flusso dati meno prevedibile
- Usa
bind:thiscon parsimonia: accedere direttamente al DOM è spesso non necessario in Svelte - I binding dimensionali hanno un costo: usano
ResizeObserverinternamente, evita di usarli su centinaia di elementi - Attenzione a
bind:innerHTML: come{@html}, è vulnerabile a XSS con input non fidati