7. Привязки (Bindings)
Привязки (Bindings) в Svelte: bind: директива 🔗
Заголовок раздела «Привязки (Bindings) в Svelte: bind: директива 🔗»Яша, одна из самых крутых фишек 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 просто генерирует этот код за тебя!
🖊️ bind:value для текстовых полей
Заголовок раздела «🖊️ bind:value для текстовых полей»Самая частая привязка — текстовые инпуты разных типов:
<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 инпут — значение остаётся строкой -->
<!-- Числовой инпут — 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)вручную!
📝 bind:value для textarea
Заголовок раздела «📝 bind:value для textarea»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:checked для чекбоксов»Для чекбоксов используй 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 для радио-кнопок
Заголовок раздела «🔘 bind:group для радио-кнопок»Когда нужно группировать радио-кнопки, используй 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вручную!
📁 bind:files для файловых полей
Заголовок раздела «📁 bind:files для файловых полей»Для <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программно из соображений безопасности браузера.
📋 Привязки для select
Заголовок раздела «📋 Привязки для select»Одиночный select
Заголовок раздела «Одиночный select»<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}Multiple select
Заголовок раздела «Multiple select»Для <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>select с объектами
Заголовок раздела «select с объектами»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}✏️ Contenteditable привязки
Заголовок раздела «✏️ Contenteditable привязки»Для редактируемых 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 — ссылка на DOM элемент
Заголовок раздела «🎯 bind:this — ссылка на DOM элемент»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при первом рендере и обновятся после монтирования.
🌐 Window привязки
Заголовок раздела «🌐 Window привязки»<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>🎵 Media привязки
Заголовок раздела «🎵 Media привязки»Для <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: работает не только с нативными элементами — можно привязываться к пропсам компонента!
<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 на компонентах
Заголовок раздела «🎯 bind:this на компонентах»bind:this работает и с компонентами — даёт доступ к экземпляру компонента и его публичным методам:
<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.
⚖️ Сравнение с React и Vue
Заголовок раздела «⚖️ Сравнение с React и Vue»Посмотрим как реализуется двусторонняя привязка в разных фреймворках:
| Задача | Svelte | React | Vue |
|---|---|---|---|
| Текстовый инпут | 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} | Контролируемые инпуты с name | v-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для чекбоксов и радио без ручного управления массивом
🎮 Интерактивный Playground
Заголовок раздела «🎮 Интерактивный Playground»Исследуй все виды привязок прямо здесь! Вводи данные и наблюдай как состояние обновляется в реальном времени 👇