Перейти к содержимому

7. Привязки (Bindings)

Яша, одна из самых крутых фишек Svelte — это двусторонняя привязка данных через директиву bind:. В React ты привык писать value={state} плюс onChange={(e) => setState(e.target.value) — целых две строки для одного поля. В Svelte достаточно написать bind:value={variable} и всё — и чтение, и запись! Давай разберём все виды привязок от простых инпутов до медиаплееров! 🎵


Обычный поток данных в Svelte (и в большинстве фреймворков) — односторонний: данные текут от переменной к DOM. bind: создаёт двусторонний поток: DOM тоже может обновлять переменную.

<script>
let name = 'Яша'
</script>
<!-- Одностороннее (только чтение из переменной): -->
<input value={name} />
<!-- name не обновляется при вводе! -->
<!-- Двустороннее (bind:): -->
<input bind:value={name} />
<!-- name обновляется автоматически при вводе ✅ -->
<p>Привет, {name}!</p>

Под капотом bind:value={name} — это сокращение от:

<input
value={name}
on:input={(e) => { name = e.target.value }}
/>

Svelte просто генерирует этот код за тебя!


Самая частая привязка — текстовые инпуты разных типов:

<script lang="ts">
let username = ''
let email = ''
let age: number = 0
let website = ''
let searchQuery = ''
</script>
<!-- Текстовый инпут -->
<input type="text" bind:value={username} placeholder="Имя пользователя" />
<p>Имя: {username}</p>
<!-- Email инпут — значение остаётся строкой -->
<input type="email" bind:value={email} placeholder="[email protected]" />
<!-- Числовой инпут — Svelte АВТОМАТИЧЕСКИ конвертирует в number! -->
<input type="number" bind:value={age} min="0" max="120" />
<p>Возраст: {age} (тип: {typeof age})</p>
<!-- typeof age === 'number', не string! Это магия Svelte ✨ -->
<!-- Range/слайдер — тоже конвертируется в number -->
<input type="range" bind:value={age} min="0" max="100" />
<!-- URL инпут -->
<input type="url" bind:value={website} placeholder="https://..." />
<!-- Поиск -->
<input type="search" bind:value={searchQuery} placeholder="Поиск..." />

💡 Важно: Для type="number" и type="range" Svelte автоматически преобразует значение в тип number. В React тебе пришлось бы писать Number(e.target.value) вручную!


Textarea работает точно так же, как обычный текстовый инпут:

<script>
let bio = ''
let maxLength = 280 // Как Твиттер 😄
</script>
<textarea
bind:value={bio}
placeholder="Расскажи о себе..."
rows={4}
maxlength={maxLength}
></textarea>
<div class="counter">
{bio.length} / {maxLength}
{#if bio.length > maxLength * 0.9}
<span class="warning">⚠️ Почти лимит!</span>
{/if}
</div>

Обрати внимание: в HTML <textarea> содержимое задаётся между тегами. В Svelte с bind:value — просто как с <input>. Никаких >{initialValue}</textarea>!


Для чекбоксов используй bind:checked вместо bind:value:

<script>
let agreed = false
let darkMode = true
let newsletter = false
</script>
<label>
<input type="checkbox" bind:checked={agreed} />
Я согласен с условиями использования
</label>
<label>
<input type="checkbox" bind:checked={darkMode} />
Тёмная тема
</label>
<label>
<input type="checkbox" bind:checked={newsletter} />
Получать новости
</label>
{#if agreed}
<p>✅ Условия приняты, добро пожаловать!</p>
{/if}
{#if darkMode}
<p>🌙 Тёмная тема включена</p>
{:else}
<p>☀️ Светлая тема</p>
{/if}

Когда нужно группировать радио-кнопки, используй bind:group — все кнопки группы привязываются к одной переменной:

<script>
let size = 'M'
let delivery = 'standard'
</script>
<!-- Группа радио-кнопок — все связаны с переменной 'size' -->
<fieldset>
<legend>Размер:</legend>
{#each ['XS', 'S', 'M', 'L', 'XL', 'XXL'] as s}
<label>
<input type="radio" bind:group={size} value={s} />
{s}
</label>
{/each}
</fieldset>
<p>Выбранный размер: {size}</p>
<!-- Другая группа независима от первой -->
<fieldset>
<legend>Доставка:</legend>
<label>
<input type="radio" bind:group={delivery} value="standard" />
Стандарт (3-5 дней)
</label>
<label>
<input type="radio" bind:group={delivery} value="express" />
Экспресс (1-2 дня)
</label>
<label>
<input type="radio" bind:group={delivery} value="same-day" />
День в день
</label>
</fieldset>
<p>Доставка: {delivery}</p>

Ключевое: все радио-кнопки группы должны иметь одинаковое значение bind:group и разные значения value.


☑️☑️ bind:group для множественных чекбоксов

Заголовок раздела «☑️☑️ bind:group для множественных чекбоксов»

bind:group работает и с чекбоксами! Когда несколько чекбоксов привязаны к одному массиву — значение checked автоматически попадает в массив и убирается из него:

<script>
let hobbies: string[] = []
let skills: string[] = ['JavaScript'] // Начальные значения
const allHobbies = ['Программирование', 'Музыка', 'Спорт', 'Кулинария', 'Путешествия']
const allSkills = ['JavaScript', 'TypeScript', 'Svelte', 'React', 'Vue', 'Node.js']
</script>
<fieldset>
<legend>Хобби (можно несколько):</legend>
{#each allHobbies as hobby}
<label>
<input type="checkbox" bind:group={hobbies} value={hobby} />
{hobby}
</label>
{/each}
</fieldset>
<!-- hobbies — это массив выбранных значений -->
<p>Выбрано хобби: {hobbies.join(', ') || 'ничего'}</p>
<p>Количество: {hobbies.length}</p>
<fieldset>
<legend>Навыки (начальные заполнены):</legend>
{#each allSkills as skill}
<label>
<input type="checkbox" bind:group={skills} value={skill} />
{skill}
</label>
{/each}
</fieldset>
<p>Навыки: [{skills.map(s => '"' + s + '"').join(', ')}]</p>

💡 Svelte автоматически управляет добавлением и удалением из массива. Никаких filter и push вручную!


Для <input type="file"> используй bind:files. Это возвращает FileList:

<script>
let files: FileList | undefined
$: if (files && files.length > 0) {
const file = files[0]
console.log('Имя файла:', file.name)
console.log('Размер:', file.size, 'байт')
console.log('Тип:', file.type)
}
function readFile() {
if (!files || files.length === 0) return
const reader = new FileReader()
reader.onload = (e) => {
console.log('Содержимое:', e.target?.result)
}
reader.readAsText(files[0])
}
</script>
<!-- Одиночный файл -->
<input type="file" bind:files />
<!-- Несколько файлов -->
<input type="file" bind:files multiple accept="image/*" />
{#if files && files.length > 0}
<p>Выбрано файлов: {files.length}</p>
{#each Array.from(files) as file}
<p>📄 {file.name} ({Math.round(file.size / 1024)} KB)</p>
{/each}
<button on:click={readFile}>Прочитать первый файл</button>
{/if}

⚠️ FileList — это только для чтения. Нельзя добавить файл в files программно из соображений безопасности браузера.


<script>
let country = ''
const countries = ['Россия', 'Германия', 'Франция', 'Япония', 'США']
</script>
<select bind:value={country}>
<option value="">— Выбери страну —</option>
{#each countries as c}
<option value={c}>{c}</option>
{/each}
</select>
{#if country}
<p>Выбрана страна: {country}</p>
{/if}

Для <select multiple> привязывается массив:

<script>
let selectedColors: string[] = []
const colors = ['Красный', 'Синий', 'Зелёный', 'Жёлтый', 'Фиолетовый']
</script>
<!-- Ctrl+клик (Windows) / Cmd+клик (Mac) для множественного выбора -->
<select bind:value={selectedColors} multiple size={5}>
{#each colors as color}
<option value={color}>{color}</option>
{/each}
</select>
<p>Выбрано: {selectedColors.join(', ') || 'ничего'}</p>

Svelte позволяет привязывать не только строки, но и объекты!

<script>
interface User { id: number; name: string; role: string }
const users: User[] = [
{ id: 1, name: 'Яша', role: 'admin' },
{ id: 2, name: 'Маша', role: 'editor' },
{ id: 3, name: 'Петя', role: 'viewer' }
]
let selectedUser: User | undefined
</script>
<select bind:value={selectedUser}>
<option value={undefined}>Выбери пользователя</option>
{#each users as user}
<!-- value — это объект, не строка! -->
<option value={user}>{user.name}</option>
{/each}
</select>
{#if selectedUser}
<p>Выбран: {selectedUser.name} (роль: {selectedUser.role})</p>
{/if}

Для редактируемых div-элементов Svelte предоставляет три варианта привязки:

<script>
let htmlContent = '<b>Жирный</b> текст'
let textContent = 'Просто текст'
let innerText = 'Ещё текст'
</script>
<!-- bind:innerHTML — сохраняет HTML-теги -->
<div contenteditable="true" bind:innerHTML={htmlContent}></div>
<p>HTML: {htmlContent}</p>
<!-- bind:textContent — только текст, без тегов -->
<div contenteditable="true" bind:textContent={textContent}></div>
<p>Text: {textContent}</p>
<!-- bind:innerText — учитывает CSS-форматирование -->
<div contenteditable="true" bind:innerText={innerText}></div>
<p>InnerText: {innerText}</p>

⚠️ Будь осторожен с bind:innerHTML! Никогда не вставляй в него пользовательский ввод без санитизации — это риск XSS-атаки. Используй DOMPurify или аналог.


bind:this — это аналог useRef в React. Он даёт прямой доступ к DOM-узлу:

<script>
import { onMount } from 'svelte'
let inputEl: HTMLInputElement
let canvasEl: HTMLCanvasElement
onMount(() => {
// После монтирования элемент доступен
inputEl.focus() // Фокус при загрузке страницы
})
function focusInput() {
inputEl.focus()
}
function measureInput() {
console.log('Ширина:', inputEl.clientWidth)
console.log('Высота:', inputEl.clientHeight)
console.log('Позиция:', inputEl.getBoundingClientRect())
}
function drawOnCanvas() {
const ctx = canvasEl.getContext('2d')!
ctx.fillStyle = '#ff3e00'
ctx.fillRect(10, 10, 100, 100)
}
onMount(() => {
drawOnCanvas()
})
</script>
<input bind:this={inputEl} placeholder="Этот инпут получит фокус" />
<button on:click={focusInput}>🎯 Дать фокус</button>
<button on:click={measureInput}>📐 Измерить</button>
<canvas bind:this={canvasEl} width={300} height={150}></canvas>

💡 Переменная inputEl будет undefined до монтирования компонента. Поэтому доступ к bind:this элементам нужен только в onMount или в обработчиках событий!


Svelte предоставляет удобные привязки для чтения размеров элемента. Они только для чтения — обновляются автоматически при изменении размера:

<script>
let divWidth: number
let divHeight: number
let divOffsetWidth: number
let divOffsetHeight: number
</script>
<div
bind:clientWidth={divWidth}
bind:clientHeight={divHeight}
bind:offsetWidth={divOffsetWidth}
bind:offsetHeight={divOffsetHeight}
style="padding: 20px; background: #1e293b; resize: both; overflow: auto; min-width: 200px;"
>
<p>Потяни за угол, чтобы изменить размер!</p>
<p>clientWidth: {divWidth}px</p>
<p>clientHeight: {divHeight}px</p>
<p>offsetWidth: {divOffsetWidth}px (включает border)</p>
<p>offsetHeight: {divOffsetHeight}px (включает border)</p>
</div>

Разница между clientWidth и offsetWidth:

  • clientWidth/clientHeight — размер содержимого + padding (без border и scrollbar)
  • offsetWidth/offsetHeight — размер содержимого + padding + border

⚠️ Размерные привязки используют ResizeObserver под капотом. Это асинхронно, поэтому значения будут 0 при первом рендере и обновятся после монтирования.


<svelte:window> позволяет привязываться к свойствам окна браузера:

<script>
let scrollY: number
let scrollX: number
let innerWidth: number
let innerHeight: number
let online: boolean
$: isMobile = innerWidth < 768
$: isWide = innerWidth >= 1200
</script>
<!-- Привязки к окну браузера -->
<svelte:window
bind:scrollY={scrollY}
bind:scrollX={scrollX}
bind:innerWidth={innerWidth}
bind:innerHeight={innerHeight}
bind:online={online}
/>
<div class="status-bar">
<span>📜 Скролл: {scrollY}px</span>
<span>📐 Окно: {innerWidth}×{innerHeight}</span>
<span>{online ? '🟢 Онлайн' : '🔴 Оффлайн'}</span>
<span>{isMobile ? '📱 Мобайл' : isWide ? '🖥️ Широкий' : '💻 Десктоп'}</span>
</div>

Доступные привязки для <svelte:window>:

  • bind:scrollX, bind:scrollY — текущая позиция прокрутки (читается и записывается!)
  • bind:innerWidth, bind:innerHeight — размеры окна (только чтение)
  • bind:outerWidth, bind:outerHeight — размеры браузера с UI (только чтение)
  • bind:online — статус подключения к интернету
<!-- Программный скролл! -->
<button on:click={() => { scrollY = 0 }}>
⬆️ Наверх страницы
</button>

Для <audio> и <video> Svelte предоставляет богатый набор привязок:

<script>
let audio: HTMLAudioElement
let currentTime = 0
let duration = 0
let paused = true
let volume = 1.0
let playbackRate = 1.0
let muted = false
$: progress = duration ? (currentTime / duration) * 100 : 0
</script>
<audio
src="/audio/track.mp3"
bind:currentTime
bind:duration
bind:paused
bind:volume
bind:playbackRate
bind:muted
bind:this={audio}
/>
<!-- Кастомный плеер -->
<div class="player">
<button on:click={() => { paused = !paused }}>
{paused ? '▶️ Играть' : '⏸️ Пауза'}
</button>
<!-- Программное управление временем -->
<input
type="range"
bind:value={currentTime}
max={duration}
step={0.1}
/>
<span>{Math.floor(currentTime / 60)}:{String(Math.floor(currentTime % 60)).padStart(2, '0')}</span>
<span>/</span>
<span>{Math.floor(duration / 60)}:{String(Math.floor(duration % 60)).padStart(2, '0')}</span>
<!-- Громкость -->
<input type="range" bind:value={volume} min="0" max="1" step="0.01" />
<!-- Скорость воспроизведения -->
<select bind:value={playbackRate}>
<option value={0.5}>0.5×</option>
<option value={1}>1×</option>
<option value={1.5}>1.5×</option>
<option value={2}>2×</option>
</select>
<label>
<input type="checkbox" bind:checked={muted} />
Без звука
</label>
</div>

Привязки для <video> те же плюс дополнительные:

  • bind:videoWidth, bind:videoHeight — размеры видео (только чтение)

bind: работает не только с нативными элементами — можно привязываться к пропсам компонента!

Counter.svelte
<script>
export let count = 0 // Экспортируем пропс — это обязательно!
function increment() { count++ }
function decrement() { count-- }
function reset() { count = 0 }
</script>
<div class="counter">
<button on:click={decrement}>−</button>
<span>{count}</span>
<button on:click={increment}>+</button>
<button on:click={reset}>↺</button>
</div>
<!-- App.svelte — привязываемся к пропсу count компонента -->
<script>
import Counter from './Counter.svelte'
let myCount = 10 // Начальное значение
</script>
<!-- bind:count создаёт двустороннюю связь с экспортированным пропсом -->
<Counter bind:count={myCount} />
<!-- myCount обновляется когда пользователь кликает в Counter -->
<p>Текущий счёт в App.svelte: {myCount}</p>
<!-- Мы тоже можем управлять значением! -->
<button on:click={() => { myCount = 0 }}>Сбросить из App</button>
<button on:click={() => { myCount = 100 }}>Установить 100</button>

Компонентные привязки — мощный инструмент, но с ними нужно быть осторожным:

<!-- ❌ Плохо — создаёт сильную связанность (tight coupling) -->
<UserForm bind:username bind:email bind:password />
<!-- Теперь UserForm "знает" о структуре родителя -->
<!-- ✅ Лучше — явные props и события -->
<UserForm
username={formData.username}
email={formData.email}
on:submit={handleSubmit}
/>

Когда стоит использовать компонентные привязки:

  • Для UI-компонентов типа <Modal bind:open>, <Accordion bind:expanded>
  • Для форм внутри одного компонента
  • Когда родитель и дочерний компонент — это одна логическая единица

Когда не стоит:

  • Через несколько уровней вложенности — лучше стор или контекст
  • Когда компонент должен быть переиспользуемым в разных контекстах
  • Для бизнес-логики — это должно быть в сторе

bind:this работает и с компонентами — даёт доступ к экземпляру компонента и его публичным методам:

Modal.svelte
<script>
let visible = false
// Экспортируем методы — родитель может вызывать их
export function open() { visible = true }
export function close() { visible = false }
export function toggle() { visible = !visible }
</script>
{#if visible}
<div class="modal-backdrop" on:click|self={close}>
<div class="modal">
<slot />
<button on:click={close}>Закрыть</button>
</div>
</div>
{/if}
<!-- App.svelte — доступ к методам через bind:this -->
<script>
import Modal from './Modal.svelte'
let modal: Modal
function showLoginModal() {
modal.open() // Вызываем метод компонента напрямую!
}
</script>
<Modal bind:this={modal}>
<h2>Войти в аккаунт</h2>
<input type="text" placeholder="Логин" />
<input type="password" placeholder="Пароль" />
<button>Войти</button>
</Modal>
<button on:click={showLoginModal}>Открыть модальное окно</button>
<button on:click={() => modal.close()}>Закрыть</button>
<button on:click={() => modal.toggle()}>Переключить</button>

💡 Экспортированные функции компонента автоматически становятся его публичным API при использовании bind:this.


Посмотрим как реализуется двусторонняя привязка в разных фреймворках:

ЗадачаSvelteReactVue
Текстовый инпутbind:value={text}value={text} onChange={(e) => setText(e.target.value)}v-model="text"
Числоbind:value={num} (авто number)value={num} onChange={(e) => setNum(+e.target.value)}v-model.number="num"
Чекбоксbind:checked={bool}checked={bool} onChange={(e) => setBool(e.target.checked)}v-model="bool"
Группа радиоbind:group={val}Контролируемые инпуты с namev-model="val"
DOM-ссылкаbind:this={el}ref={elRef}ref="el" / v-bind:ref
Размеры элементаbind:clientWidth={w}useRef + ResizeObserverНет встроенного
Window scroll<svelte:window bind:scrollY>useEffect + addEventListenerНет встроенного
Компонентныйbind:propName={val}Не поддерживаетсяv-model на компонентах

Ключевые преимущества Svelte bind::

  • ✅ Минимальный бойлерплейт — одна строка вместо двух
  • ✅ Авто-конвертация типов для number/range
  • ✅ Встроенные привязки для размеров, скролла, медиа
  • bind:group для чекбоксов и радио без ручного управления массивом

Исследуй все виды привязок прямо здесь! Вводи данные и наблюдай как состояние обновляется в реальном времени 👇