16. Директивы
🔧 Директивы в Solid.js
Заголовок раздела «🔧 Директивы в Solid.js»Яша, директивы в Solid — это концепция, вдохновлённая Vue и Angular, но реализованная по-настоящему элегантно. Вместо HOC или хуков для работы с DOM — просто функция + use:directive. Хочешь drag-and-drop? Tooltip? Click-outside? Intersection Observer? Всё это — директивы! 🎯
🏗️ Анатомия директивы
Заголовок раздела «🏗️ Анатомия директивы»Директива — это функция с двумя аргументами:
// Сигнатура директивыtype DirectiveFn<T = undefined> = ( element: HTMLElement, // DOM-элемент accessor: () => T // getter для параметра (реактивный!)) => void;
// Простейшая директива — автофокусfunction autofocus(el: HTMLElement, _accessor: () => undefined) { el.focus();}
// Использованиеfunction App() { return <input use:autofocus />;}💡
accessor— это функция, а не значение. Вызови еёaccessor(), чтобы получить текущее значение. Это нужно для реактивности!
📤 Передача параметров
Заголовок раздела «📤 Передача параметров»// Директива с параметромfunction tooltip(el: HTMLElement, accessor: () => string) { const tooltipEl = document.createElement('div'); tooltipEl.className = 'tooltip';
// Реактивно обновляем текст при изменении параметра createEffect(() => { tooltipEl.textContent = accessor(); // вызываем для реактивности });
el.addEventListener('mouseenter', () => { document.body.appendChild(tooltipEl); const rect = el.getBoundingClientRect(); tooltipEl.style.top = `${rect.top - 30}px`; tooltipEl.style.left = `${rect.left}px`; });
el.addEventListener('mouseleave', () => { tooltipEl.remove(); });
// Очистка при размонтировании onCleanup(() => tooltipEl.remove());}
// Использованиеfunction App() { const [text, setText] = createSignal('Наведи на меня!');
return ( <> <button use:tooltip={text()}>Кнопка с тултипом</button> <input onInput={(e) => setText(e.target.value)} /> </> );}🖱️ Click Outside
Заголовок раздела «🖱️ Click Outside»Классический пример — закрытие модального окна при клике снаружи:
import { onCleanup } from 'solid-js';
function clickOutside(el: HTMLElement, accessor: () => () => void) { const handler = (e: MouseEvent) => { if (!el.contains(e.target as Node)) { accessor()(); // accessor() → callback, callback() → вызов } };
document.addEventListener('click', handler, { capture: true }); onCleanup(() => document.removeEventListener('click', handler, { capture: true }));}
// Использованиеfunction Modal() { const [open, setOpen] = createSignal(false);
return ( <Show when={open()}> <div class="overlay"> <div class="modal" use:clickOutside={() => setOpen(false)} > <h2>Модальное окно</h2> <p>Кликни снаружи, чтобы закрыть</p> </div> </div> </Show> );}🗂️ Draggable директива
Заголовок раздела «🗂️ Draggable директива»function draggable(el: HTMLElement, accessor: () => { onMove?: (x: number, y: number) => void }) { let startX = 0, startY = 0; let startLeft = 0, startTop = 0; let isDragging = false;
const onMouseDown = (e: MouseEvent) => { isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = el.offsetLeft; startTop = el.offsetTop; el.style.cursor = 'grabbing'; e.preventDefault(); };
const onMouseMove = (e: MouseEvent) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; el.style.left = `${startLeft + dx}px`; el.style.top = `${startTop + dy}px`; accessor().onMove?.(startLeft + dx, startTop + dy); };
const onMouseUp = () => { isDragging = false; el.style.cursor = 'grab'; };
el.style.position = 'absolute'; el.style.cursor = 'grab'; el.addEventListener('mousedown', onMouseDown); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp);
onCleanup(() => { el.removeEventListener('mousedown', onMouseDown); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); });}
// Использованиеfunction App() { const [pos, setPos] = createSignal({ x: 100, y: 100 });
return ( <div style={{ position: 'relative', height: '400px' }}> <div use:draggable={{ onMove: (x, y) => setPos({ x, y }) }} style={{ width: '100px', height: '100px', background: '#2c67d5' }} > Тащи меня! </div> <p>Позиция: x={pos().x}, y={pos().y}</p> </div> );}👁️ Intersection Observer
Заголовок раздела «👁️ Intersection Observer»function intersectionObserver( el: HTMLElement, accessor: () => { threshold?: number; onVisible?: () => void; onHidden?: () => void }) { const observer = new IntersectionObserver( (entries) => { const { onVisible, onHidden } = accessor(); entries.forEach(entry => { if (entry.isIntersecting) onVisible?.(); else onHidden?.(); }); }, { threshold: accessor().threshold ?? 0.5 } );
observer.observe(el); onCleanup(() => observer.disconnect());}
// Использование — ленивая загрузкаfunction LazyImage({ src }: { src: string }) { const [loaded, setLoaded] = createSignal(false);
return ( <div use:intersectionObserver={{ threshold: 0.1, onVisible: () => setLoaded(true), }} style={{ min-height: '200px' }} > <Show when={loaded()}> <img src={src} alt="Lazy loaded" /> </Show> </div> );}🚨 TypeScript и директивы
Заголовок раздела «🚨 TypeScript и директивы»// Чтобы TypeScript знал о директиве — декларация модуляdeclare module 'solid-js' { namespace JSX { interface Directives { tooltip: string; clickOutside: () => void; draggable: { onMove?: (x: number, y: number) => void }; intersectionObserver: { threshold?: number; onVisible?: () => void }; } }}
// Теперь TypeScript проверяет типы параметров:<div use:tooltip="Привет!">OK</div><div use:tooltip={42}>Ошибка!</div> // Type error!