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

6. События и диспетчеризация

Яша, давай разберёмся с событиями в Svelte — это одна из тех вещей, которые делают Svelte таким приятным для работы! По сравнению с React, где нужно писать onClick={handler}, Svelte использует специальную директиву on: которая читается почти как обычный HTML. Плюс — мощные модификаторы, которые избавляют от необходимости писать e.preventDefault() руками. Поехали! 🚀


В Svelte для подписки на DOM-события используется директива on:имяСобытия={обработчик}. Синтаксис максимально читаемый:

<script>
function handleClick() {
alert('Кнопка нажата!')
}
function handleInput(event) {
console.log('Введено:', event.target.value)
}
function handleKeydown(event) {
if (event.key === 'Enter') {
console.log('Enter нажат!')
}
}
</script>
<!-- Клик -->
<button on:click={handleClick}>Нажми меня</button>
<!-- Ввод текста -->
<input on:input={handleInput} placeholder="Введи текст..." />
<!-- Клавиатура -->
<input on:keydown={handleKeydown} placeholder="Нажми Enter" />

Обрати внимание: обработчик передаётся без скобок handleClick, а не handleClick(). Если добавишь скобки — функция вызовется при рендере, а не по клику!

Svelte поддерживает все стандартные DOM-события: on:click, on:input, on:change, on:keydown, on:keyup, on:keypress, on:focus, on:blur, on:mouseenter, on:mouseleave, on:mousemove, on:submit, on:scroll, и многие другие.


Иногда удобно написать обработчик прямо в шаблоне — для простых операций это отлично работает:

<script>
let count = 0
let name = ''
</script>
<!-- Инлайн: простое выражение -->
<button on:click={() => count++}>
Счётчик: {count}
</button>
<!-- Инлайн с аргументом события -->
<button on:click={(e) => console.log('Target:', e.target)}>
Залогировать
</button>
<!-- Инлайн для инпута -->
<input
on:input={(e) => (name = e.target.value)}
placeholder="Введи имя"
/>
<p>Привет, {name || 'незнакомец'}!</p>

💡 Совет Яши: Инлайн-обработчики удобны для простых действий вроде count++ или обновления одной переменной. Для более сложной логики лучше выносить в отдельную функцию — код читается чище!


Когда ты используешь TypeScript (а ты же используешь TypeScript, правда? 😄), Svelte позволяет типизировать объект события:

<script lang="ts">
function handleClick(event: MouseEvent) {
console.log('Позиция клика:', event.clientX, event.clientY)
const target = event.target as HTMLButtonElement
console.log('Текст кнопки:', target.textContent)
}
function handleInput(event: Event) {
const input = event.target as HTMLInputElement
console.log('Значение:', input.value)
}
function handleKeydown(event: KeyboardEvent) {
console.log('Нажатая клавиша:', event.key)
console.log('С Ctrl?', event.ctrlKey)
console.log('С Shift?', event.shiftKey)
}
function handleSubmit(event: SubmitEvent) {
event.preventDefault()
const form = event.target as HTMLFormElement
const data = new FormData(form)
console.log('Имя:', data.get('name'))
}
</script>
<button on:click={handleClick}>Кликни и посмотри координаты</button>
<input on:input={handleInput} placeholder="Типизированный ввод" />
<input on:keydown={handleKeydown} placeholder="Типизированная клавиатура" />
<form on:submit={handleSubmit}>
<input name="name" placeholder="Введи имя" />
<button type="submit">Отправить</button>
</form>

Типы событий, которые тебе чаще всего понадобятся:

  • MouseEvent — для click, mouseenter, mouseleave, mousemove
  • KeyboardEvent — для keydown, keyup, keypress
  • InputEvent или Event — для input
  • FocusEvent — для focus, blur
  • SubmitEvent — для submit
  • WheelEvent — для wheel
  • TouchEvent — для сенсорных событий

Вот где Svelte блистает! Модификаторы добавляются через символ | сразу после имени события и встроенно выполняют типичные операции с событием. Больше не нужно писать e.preventDefault() в каждом обработчике!

|preventDefault — блокирует стандартное поведение

Заголовок раздела «|preventDefault — блокирует стандартное поведение»
<!-- Без модификатора: -->
<form on:submit={(e) => { e.preventDefault(); handleSubmit() }}>...</form>
<!-- С модификатором: -->
<form on:submit|preventDefault={handleSubmit}>...</form>
<!-- Ссылка, которая не переходит по URL: -->
<a href="https://example.com" on:click|preventDefault={() => alert('Переход заблокирован!')}>
Не уходи!
</a>
<div on:click={() => console.log('Внешний div')}>
<!-- Без модификатора — родитель тоже получит клик -->
<button on:click={() => console.log('Кнопка')}>Без модификатора</button>
<!-- С модификатором — клик не всплывёт до div -->
<button on:click|stopPropagation={() => console.log('Только кнопка')}>
С |stopPropagation
</button>
</div>
<script>
let welcomed = false
function showWelcome() {
welcomed = true
}
</script>
<!-- После первого клика слушатель автоматически удаляется -->
<button on:click|once={showWelcome}>
{welcomed ? '✅ Уже поприветствовал!' : 'Поприветствуй меня (один раз)'}
</button>

💡 Очень полезно для кнопок “Принять условия”, туториалов, и любых действий, которые должны произойти ровно один раз.

|passive — подсказка браузеру для оптимизации

Заголовок раздела «|passive — подсказка браузеру для оптимизации»
<!-- Сообщаем браузеру что обработчик НЕ вызовет preventDefault -->
<!-- Браузер может оптимизировать прокрутку, не дожидаясь обработчика -->
<div on:scroll|passive={handleScroll}>
Скроллируемый контент...
</div>
<!-- Особенно полезно для touch-событий -->
<div on:touchmove|passive={handleTouchMove}>
Свайпаемый элемент...
</div>

|passive не изменяет поведение события — он просто обещает браузеру, что preventDefault() вызван не будет. Это улучшает производительность прокрутки на мобильных устройствах!

<!-- Обработчик срабатывает на фазе захвата (capture phase) -->
<!-- Это происходит до того, как событие доходит до цели -->
<div on:click|capture={() => console.log('Поймано на пути вниз!')}>
<button on:click={() => console.log('Достигло цели')}>
Кнопка
</button>
</div>
<!-- Порядок: "Поймано на пути вниз!" → "Достигло цели" -->

|trusted — только реальные пользовательские события

Заголовок раздела «|trusted — только реальные пользовательские события»
<script>
function handleRealClick() {
console.log('Это настоящий клик пользователя!')
}
</script>
<!-- Срабатывает только если event.isTrusted === true -->
<!-- Программные вызовы button.click() будут проигнорированы -->
<button on:click|trusted={handleRealClick}>
Только реальный клик
</button>
<!-- Срабатывает только если пользователь кликнул именно на этот элемент -->
<!-- Клики на дочерние элементы будут проигнорированы -->
<div
class="modal-backdrop"
on:click|self={closeModal}
>
<!-- Клик на контент не закроет модалку -->
<div class="modal-content">
<p>Контент модалки</p>
<button on:click={closeModal}>✕ Закрыть</button>
</div>
</div>

Модификаторы можно комбинировать! Просто добавляй их через | один за другим:

<!-- preventDefault + stopPropagation вместе -->
<a href="#" on:click|preventDefault|stopPropagation={handleLink}>
Ссылка без перехода и без всплытия
</a>
<!-- once + preventDefault -->
<form on:submit|once|preventDefault={handleFirstSubmit}>
<button type="submit">Отправить (один раз)</button>
</form>
<!-- capture + stopPropagation -->
<div on:click|capture|stopPropagation={handleCapture}>
Поймаю и остановлю!
</div>

Когда нужно передать событие из дочернего компонента в родительский, Svelte предоставляет createEventDispatcher. Это аналог $emit из Vue или пропсов-коллбэков в React:

ChildButton.svelte
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
function handleClick() {
// Отправляем событие 'notify' с данными
dispatch('notify', {
message: 'Кнопка нажата!',
timestamp: new Date().toISOString()
})
}
function handleError() {
dispatch('error', {
code: 404,
message: 'Что-то пошло не так'
})
}
</script>
<button on:click={handleClick}>Нажми меня!</button>
<button on:click={handleError}>Симулировать ошибку</button>

Функция dispatch принимает два аргумента:

  1. Имя события (строка) — любое имя, которое ты придумаешь
  2. Данные события (любой тип) — то, что будет доступно в event.detail

В родительском компоненте подписываемся точно так же, как на DOM-события — через on::

Parent.svelte
<script>
import ChildButton from './ChildButton.svelte'
let lastMessage = ''
let errorInfo = null
function handleNotify(event) {
// Данные доступны в event.detail
lastMessage = event.detail.message
console.log('Время:', event.detail.timestamp)
}
function handleError(event) {
errorInfo = event.detail
console.error('Ошибка:', event.detail.code, event.detail.message)
}
</script>
<ChildButton
on:notify={handleNotify}
on:error={handleError}
/>
{#if lastMessage}
<p>📩 Последнее сообщение: {lastMessage}</p>
{/if}
{#if errorInfo}
<p class="error">❌ Ошибка {errorInfo.code}: {errorInfo.message}</p>
{/if}

💡 event.detail — это стандартное свойство CustomEvent в браузере. Svelte использует нативный механизм кастомных событий!


Иногда нужно “пробросить” событие через промежуточный компонент — например, если ты создаёшь компонент-обёртку <Button> над нативным <button>. Вместо того чтобы вручную создавать dispatcher и обработчик, можно использовать форвардинг:

<!-- FancyButton.svelte — просто добавь on:click без обработчика -->
<script>
// Никакой логики не нужно!
</script>
<button class="fancy-button" on:click on:focus on:blur>
<slot />
</button>
<!-- Стили... -->
<style>
.fancy-button {
background: #ff3e00;
color: white;
border: none;
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
</style>
<!-- App.svelte — родитель использует FancyButton как обычный button -->
<script>
import FancyButton from './FancyButton.svelte'
function handleClick(event) {
console.log('Клик получен в App.svelte!', event)
}
</script>
<!-- on:click прозрачно форвардится от FancyButton.svelte -->
<FancyButton on:click={handleClick}>
Нажми!
</FancyButton>

Ты можешь форвардить несколько событий одновременно: on:click on:mouseenter on:focus — каждое без обработчика. Это невероятно удобно для UI-библиотек!

Форвардинг работает и для кастомных событий от createEventDispatcher:

<!-- MiddleLayer.svelte — пробрасывает событие 'message' дальше -->
<script>
import DeepChild from './DeepChild.svelte'
</script>
<!-- on:message без обработчика = форвардинг вверх по дереву -->
<DeepChild on:message />

В TypeScript можно строго типизировать кастомные события компонента с помощью ComponentEvents и дженериков createEventDispatcher:

<script lang="ts">
import { createEventDispatcher } from 'svelte'
// Описываем все события компонента с типами данных
type Events = {
notify: { message: string; timestamp: string }
close: null
submit: { formData: Record<string, string> }
}
// Передаём тип в дженерик
const dispatch = createEventDispatcher<Events>()
function handleSubmit() {
// TypeScript проверяет: второй аргумент должен соответствовать типу
dispatch('notify', {
message: 'Форма отправлена!',
timestamp: new Date().toISOString()
})
}
function handleClose() {
// Для событий без данных передаём null
dispatch('close', null)
}
</script>
<!-- Parent.svelte с типизацией -->
<script lang="ts">
import MyForm from './MyForm.svelte'
import type { ComponentEvents } from 'svelte'
// Получаем типы событий компонента
type MyFormEvents = ComponentEvents<MyForm>
function handleNotify(event: MyFormEvents['notify']) {
// TypeScript знает структуру event.detail!
console.log(event.detail.message) // string ✅
console.log(event.detail.timestamp) // string ✅
}
</script>
<MyForm on:notify={handleNotify} on:close={() => console.log('Закрыто')} />

В Svelte 5 появились руны (runes), и подход к событиям изменился! Теперь вместо createEventDispatcher рекомендуется использовать пропсы-коллбэки — как в React:

<!-- Button.svelte в Svelte 5 -->
<script>
// Объявляем пропс-коллбэк через $props()
let { onclick, children } = $props()
</script>
<button {onclick}>
{@render children()}
</button>
<!-- Или с типизацией -->
<script lang="ts">
interface Props {
onclick?: (event: MouseEvent) => void
onnotify?: (data: { message: string }) => void
children?: import('svelte').Snippet
}
let { onclick, onnotify, children }: Props = $props()
function handleClick(event: MouseEvent) {
onclick?.(event)
onnotify?.({ message: 'Кнопка нажата!' })
}
</script>
<button on:click={handleClick}>
{@render children?.()}
</button>
<!-- Использование в родителе — как обычный HTML! -->
<Button onclick={() => console.log('Клик!')} />

⚠️ Совет Яши: Если ты только начинаешь изучать Svelte, освой сначала createEventDispatcher (Svelte 4). В Svelte 5 они тоже работают, просто не рекомендуются. Callback props — это будущее!


Посмотрим, как одно и то же реализуется в разных фреймворках — это помогает понять, в чём уникальность Svelte:

ЗадачаSvelteReactVue
Кликon:click={fn}onClick={fn}@click="fn"
Предотвратить переходon:click|preventDefaultonClick={(e) => e.preventDefault()}@click.prevent
Только один разon:click|onceНужен useEffect + removeEventListener@click.once
Остановить всплытиеon:click|stopPropagationonClick={(e) => e.stopPropagation()}@click.stop
Кастомные событияcreateEventDispatcherПропсы-коллбэки$emit
Форвардингon:click без обработчикаНужен spreад {...props}v-on="$listeners"

Ключевые отличия:

  • Svelte — модификаторы встроены в синтаксис (|once, |passive), очень декларативно
  • React — всё через JavaScript, нет специального синтаксиса для модификаторов
  • Vue — модификаторы через точку (@click.prevent), похоже на Svelte, но другой синтаксис

Svelte ближе всего к Vue в плане читаемости, но синтаксис on:событие напоминает нативный HTML-атрибут onclick, что делает код очень понятным!


Поиграй с событиями прямо здесь! Переключайся между вкладками и нажимай на элементы чтобы увидеть как работают разные виды событий в реальном времени 👇