13. Actions
🎮 Svelte Actions
Заголовок раздела «🎮 Svelte Actions»Action — это функция, которая получает DOM-узел и делает с ним что угодно. Это как хук жизненного цикла, привязанный к конкретному элементу. Хочешь tooltip? Drag and drop? Lazy loading? Actions — твой инструмент 🔧
Зачем Actions?
Заголовок раздела «Зачем Actions?»Проблема без actions:- Логика работы с DOM размазана по onMount/onDestroy- Нельзя переиспользовать между компонентами- Нужно хранить ref на элемент вручную
Решение с actions:✅ Логика инкапсулирована в одной функции✅ Переиспользуется директивой use:✅ Автоматическая очистка (destroy)✅ Реакция на изменение параметров (update)Анатомия Action
Заголовок раздела «Анатомия Action»import type { Action } from 'svelte/action'
// Типизированный action// Action<ElementType, ParametersType, EventsType>const myAction: Action<HTMLElement, { color: string }> = (node, params) => { // node — DOM элемент, к которому применяется action // params — начальные параметры
// Инициализация node.style.color = params?.color ?? 'red'
return { // Вызывается при изменении параметров update(newParams) { node.style.color = newParams.color },
// Вызывается при удалении элемента из DOM destroy() { // Очищаем: удаляем обработчики, таймеры, subscription'ы } }}<!-- Использование --><script> import { myAction } from './actions' let color = 'blue'</script>
<!-- Без параметров --><div use:myAction>...</div>
<!-- С параметрами --><div use:myAction={{ color }}>...</div>Action: clickOutside
Заголовок раздела «Action: clickOutside»Классика! Закрывать модальные окна при клике за пределами:
import type { Action } from 'svelte/action'
interface ClickOutsideAttributes { 'on:clickoutside': (event: CustomEvent) => void}
export const clickOutside: Action<HTMLElement, undefined, ClickOutsideAttributes> = (node) => { function handleClick(event: MouseEvent) { if (node && !node.contains(event.target as Node)) { node.dispatchEvent(new CustomEvent('clickoutside')) } }
document.addEventListener('click', handleClick, true) // capture phase!
return { destroy() { document.removeEventListener('click', handleClick, true) } }}<script> import { clickOutside } from './actions/clickOutside' let dropdownOpen = false</script>
<div use:clickOutside on:clickoutside={() => dropdownOpen = false}> <button on:click={() => dropdownOpen = !dropdownOpen}> Меню ▼ </button>
{#if dropdownOpen} <ul class="dropdown"> <li>Профиль</li> <li>Настройки</li> <li>Выйти</li> </ul> {/if}</div>Action: longPress
Заголовок раздела «Action: longPress»import type { Action } from 'svelte/action'
interface LongPressOptions { duration?: number}
interface LongPressAttributes { 'on:longpress': (event: CustomEvent<{ duration: number }>) => void}
export const longPress: Action<HTMLElement, LongPressOptions, LongPressAttributes> = ( node, options = {}) => { let { duration = 500 } = options let timer: ReturnType<typeof setTimeout> let startTime: number
function handleMouseDown() { startTime = Date.now() timer = setTimeout(() => { node.dispatchEvent( new CustomEvent('longpress', { detail: { duration: Date.now() - startTime } }) ) }, duration) }
function handleMouseUp() { clearTimeout(timer) }
node.addEventListener('mousedown', handleMouseDown) node.addEventListener('mouseup', handleMouseUp) node.addEventListener('touchstart', handleMouseDown) node.addEventListener('touchend', handleMouseUp)
return { update(newOptions) { duration = newOptions.duration ?? 500 }, destroy() { clearTimeout(timer) node.removeEventListener('mousedown', handleMouseDown) node.removeEventListener('mouseup', handleMouseUp) node.removeEventListener('touchstart', handleMouseDown) node.removeEventListener('touchend', handleMouseUp) } }}<script> import { longPress } from './actions/longPress'
function handleLongPress(event: CustomEvent<{ duration: number }>) { alert(`Зажато на ${event.detail.duration}мс!`) }</script>
<button use:longPress={{ duration: 800 }} on:longpress={handleLongPress}> Зажми меня! 🤌</button>Action: tooltip
Заголовок раздела «Action: tooltip»import type { Action } from 'svelte/action'
interface TooltipOptions { text: string position?: 'top' | 'bottom' | 'left' | 'right' delay?: number}
export const tooltip: Action<HTMLElement, TooltipOptions> = (node, options) => { let tooltipEl: HTMLDivElement | null = null let timer: ReturnType<typeof setTimeout>
function createTooltip() { tooltipEl = document.createElement('div') tooltipEl.className = 'svelte-tooltip' tooltipEl.textContent = options.text
// Позиционирование const rect = node.getBoundingClientRect() const position = options.position ?? 'top'
Object.assign(tooltipEl.style, { position: 'fixed', background: '#1e293b', color: '#e2e8f0', padding: '4px 8px', borderRadius: '4px', fontSize: '12px', pointerEvents: 'none', zIndex: '9999', top: position === 'top' ? `${rect.top - 30}px` : `${rect.bottom + 8}px`, left: `${rect.left + rect.width / 2}px`, transform: 'translateX(-50%)', })
document.body.appendChild(tooltipEl) }
function handleMouseEnter() { timer = setTimeout(createTooltip, options.delay ?? 300) }
function handleMouseLeave() { clearTimeout(timer) tooltipEl?.remove() tooltipEl = null }
node.addEventListener('mouseenter', handleMouseEnter) node.addEventListener('mouseleave', handleMouseLeave)
return { update(newOptions) { options = newOptions if (tooltipEl) tooltipEl.textContent = newOptions.text }, destroy() { clearTimeout(timer) tooltipEl?.remove() node.removeEventListener('mouseenter', handleMouseEnter) node.removeEventListener('mouseleave', handleMouseLeave) } }}<button use:tooltip={{ text: 'Удалить навсегда! ⚠️', position: 'top' }}> 🗑️ Удалить</button>
<button use:tooltip={{ text: 'Сохранить в облако', delay: 100 }}> 💾 Сохранить</button>Action: autofocus
Заголовок раздела «Action: autofocus»import type { Action } from 'svelte/action'
export const autofocus: Action<HTMLElement> = (node) => { // Ждём следующего тика (элемент должен быть видимым) const frame = requestAnimationFrame(() => { if (node instanceof HTMLElement) { node.focus() } })
return { destroy() { cancelAnimationFrame(frame) } }}{#if showModal} <div class="modal"> <!-- Автоматически получает фокус при появлении! --> <input use:autofocus placeholder="Введи что-нибудь" /> </div>{/if}Action: lazyLoad (Intersection Observer)
Заголовок раздела «Action: lazyLoad (Intersection Observer)»import type { Action } from 'svelte/action'
interface LazyLoadOptions { src: string placeholder?: string threshold?: number}
export const lazyLoad: Action<HTMLImageElement, LazyLoadOptions> = ( node, options) => { const { placeholder = '/placeholder.jpg', threshold = 0.1 } = options
// Пока не видим — показываем placeholder node.src = placeholder
const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { node.src = options.src node.onload = () => node.classList.add('loaded') observer.unobserve(node) } }) }, { threshold } )
observer.observe(node)
return { update(newOptions) { options = newOptions }, destroy() { observer.unobserve(node) } }}{#each photos as photo} <img use:lazyLoad={{ src: photo.url, placeholder: photo.thumbnail }} alt={photo.title} class="photo" />{/each}Action: draggable
Заголовок раздела «Action: draggable»import type { Action } from 'svelte/action'
interface DraggableOptions { axis?: 'x' | 'y' | 'both' bounds?: HTMLElement | 'parent' | 'window'}
interface DraggableAttributes { 'on:dragstart': (e: CustomEvent<{x: number; y: number}>) => void 'on:drag': (e: CustomEvent<{x: number; y: number}>) => void 'on:dragend': (e: CustomEvent<{x: number; y: number}>) => void}
export const draggable: Action<HTMLElement, DraggableOptions, DraggableAttributes> = ( node, options = {}) => { let startX: number, startY: number let initialLeft: number, initialTop: number let isDragging = false
node.style.position = 'absolute' node.style.cursor = 'grab'
function onMouseDown(event: MouseEvent) { isDragging = true startX = event.clientX startY = event.clientY initialLeft = node.offsetLeft initialTop = node.offsetTop node.style.cursor = 'grabbing'
node.dispatchEvent(new CustomEvent('dragstart', { detail: { x: initialLeft, y: initialTop } }))
document.addEventListener('mousemove', onMouseMove) document.addEventListener('mouseup', onMouseUp) }
function onMouseMove(event: MouseEvent) { if (!isDragging) return
const deltaX = event.clientX - startX const deltaY = event.clientY - startY const { axis = 'both' } = options
const newLeft = axis !== 'y' ? initialLeft + deltaX : initialLeft const newTop = axis !== 'x' ? initialTop + deltaY : initialTop
node.style.left = `${newLeft}px` node.style.top = `${newTop}px`
node.dispatchEvent(new CustomEvent('drag', { detail: { x: newLeft, y: newTop } })) }
function onMouseUp() { isDragging = false node.style.cursor = 'grab'
node.dispatchEvent(new CustomEvent('dragend', { detail: { x: node.offsetLeft, y: node.offsetTop, } }))
document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) }
node.addEventListener('mousedown', onMouseDown)
return { update(newOptions) { options = newOptions }, destroy() { node.removeEventListener('mousedown', onMouseDown) document.removeEventListener('mousemove', onMouseMove) document.removeEventListener('mouseup', onMouseUp) } }}Actions с параметрами — реактивность
Заголовок раздела «Actions с параметрами — реактивность»<script> import { tooltip } from './actions/tooltip' import { longPress } from './actions/longPress'
let tooltipText = 'Начальный текст' let longPressDuration = 500
// Параметры реактивны! При изменении переменных // вызывается action.update()</script>
<input bind:value={tooltipText} /><button use:tooltip={{ text: tooltipText }}>Наведи мышь</button>
<input type="range" min={200} max={2000} bind:value={longPressDuration} /><button use:longPress={{ duration: longPressDuration }}> Зажми ({longPressDuration}мс)</button>Actions vs Event Directives
Заголовок раздела «Actions vs Event Directives»Когда использовать use: (action):✅ Логика работы с DOM (фокус, позиция, размер)✅ Сторонние библиотеки (chart.js, maps)✅ Intersection/Resize Observer✅ Drag and drop✅ Tooltip, popover, dropdown✅ Keyboard shortcuts✅ Переиспользуемая DOM-логика
Когда использовать on: (event directive):✅ Простые обработчики событий✅ Встроенные DOM события (click, input, keydown)✅ Одноразовая логика в компоненте
Когда НЕ нужен action:❌ Только изменить стиль → используй class:name или style:❌ Простое условие → используй {#if}❌ Реактивное значение → используй $:Сторонние библиотеки через Actions
Заголовок раздела «Сторонние библиотеки через Actions»// actions/chart.ts — интеграция Chart.jsimport type { Action } from 'svelte/action'import type { Chart, ChartData } from 'chart.js'
export const chart: Action<HTMLCanvasElement, ChartData> = (node, data) => { let chartInstance: Chart | null = null
import('chart.js').then(({ Chart, registerables }) => { Chart.register(...registerables) chartInstance = new Chart(node, { type: 'bar', data, options: { responsive: true, animation: { duration: 500 } } }) })
return { update(newData) { if (chartInstance) { chartInstance.data = newData chartInstance.update() } }, destroy() { chartInstance?.destroy() } }}<script> import { chart } from './actions/chart'
$: chartData = { labels: months, datasets: [{ label: 'Продажи', data: sales }] }</script>
<canvas use:chart={chartData}></canvas>