Недавно на Хабре появилась статья от @sanReal, где Александр рассказал о том, каким приёмам и каким возможностям Svelte он научился на собственном опыте. Я был немного удивлён не увидев в его списке упоминания одного из самых мощных инструментов фреймворка — Действий. К тому же, общаясь с людьми в сообществе @sveltejs, которые уже создают очень хорошие приложения при помощи Svelte, я иногда замечаю, что не все пользуются Действиями даже там, где их применение идеально решало бы задачу. В этой статье я расскажу, что такое Действия и на простейших примерах покажу их применение.
Предположим, у нас есть текстовое поле, в которое пользователь должен ввести номер телефона. Нам нужно добиться того, чтобы пользователь смог ввести в поле только цифры.
Телефон: +7 <input type="text" />
Когда встречается задача наделить стандартный HTML-элемент дополнительными возможностями, обычной практикой при работе в любом компонентом фреймворке будет создание переиспользуемого компонента вокруг этого элемента, внутри которого реализуется логика нестандартного поведения. Так же можно поступить и в Svelte:
<!-- InputDigits.svelte -->
<script>
function clean_value(e){
e.target.value = e.target.value.replace(/[^\d]/g,'');
}
</script>
<input type="text" on:input={clean_value} />
<!-- App.svelte -->
...
Телефон: +7 <InputDigits />
Задача выполнена, но сам элемент <input/>
теперь спрятан внутри компонента, поэтому мы больше не сможем манипулировать им как обычным HTML-элементом — "навесить" на него какие-либо CSS-классы, обработчики событий или какие-то особенные атрибуты будет уже не так просто.
Для подобных случаев в Svelte есть свой особенный подход — Действия (в английском варианте Actions). В официальном учебнике Svelte можно найти урок, посвященный этой теме, но, на мой взгляд, пример подобран несколько громоздкий — вся "соль" Действий может в нем затеряться и ускользнуть от новичков, которые выполняют этот урок. На самом же деле концепция очень простая — это что-то вроде функции жизненного цикла для любого HTML-элемента. Она вызывается, когда элемент монтируется в DOM. В качестве аргумента функция получает ссылку на соответствующий узел DOM-дерева, с которым затем и происходит вся работа.
Перепишем наш пример с использованием Действия:
<script>
function onlydigits(node) {
function clean_value(){
node.value = node.value.replace(/[^\d]/g,'');
}
node.addEventListener('input',clean_value);
return {
destroy: ()=>node.removeEventListener('input',clean_value)
}
}
</script>
Телефон: +7 <input type="text" use:onlydigits />
Мы создали функцию onlydigits
, которая и будет нашим Действием. Её работа заключается в том, чтобы добавить DOM-узлу node
обработчик для события input
. Обратите внимание, что функция возвращает объект с методом destroy
, который будет вызван при удалении элемента из DOM-дерева, позволив нам убрать обработчик события с элемента и предотвратить возможные утечки памяти.
Чтобы указать Svelte, что для какого-либо HTML-элемента мы хотим использовать некое Действие, существует директива use
. В примере мы назначили Действие на элемент <input/>
директивой use:onlydigits
.
На один элемент можно назначить сразу несколько Действий. Сделаем так, чтобы пользователь не мог ввести более 10 цифр:
<script>
function onlydigits(node) { ... }
function max10symbols(node) {
function trim_value(){
node.value = node.value.substring(0, 10);
}
node.addEventListener('input',trim_value);
return {
destroy: ()=>node.removeEventListener('input',trim_value)
}
}
</script>
Телефон: +7 <input type="text" use:onlydigits use:max10symbols />
Теперь к элементу <input />
привязано два Действия — use:onlydigits
и use:max10symbols
. Порядок вызова функций зависит от порядка объявления директив.
В директиве use
можно указать дополнительный параметр, который будет передан в функцию Действия вторым аргументом. Переработаем наш пример так, чтобы пользователь смог вводить значение только по маске. Также добавим еще одно текстовое поле для указания номера паспорта:
<script>
function format_by_pattern(node,pattern) {
function set_cursor(){
const match = node.value.match(/[\d]/gi);
const pos = match ? node.value.lastIndexOf(match.pop())+1 : 0;
node.setSelectionRange(pos, pos);
}
function format_value(e){
let digits = node.value.replace(/[^\d]/g,'').split('');
node.value = pattern.replace(/[*]/g,(m)=>digits.shift()||m);
set_cursor();
}
node.addEventListener('input',format_value);
format_value();
return {
destroy: ()=>node.removeEventListener('input',format_value)
}
}
</script>
Телефон: +7 <input type="text" use:format_by_pattern={'(***) ***-**-**'} /> <br/>
Паспорт: <input type="text" use:format_by_pattern={'серия **** №******'} />
Оба <input />
всё еще являются обычными HTML-элементами. К каждому из них назначена одна и та же функция Действия format_by_pattern
, но в качестве параметра мы передаём разные маски для телефона и номера паспорта. В фигурных скобках, как и везде в шаблонах Svelte, может быть любое валидное JavaScript-выражение.
В последний раз усложним наш пример, предположив, что пользователь может вводить либо номер гражданского паспорта, либо заграничного. При выборе типа паспорта должна меняться и маска для ввода номера.
<script>
function format_by_pattern(node,pattern) {
function set_cursor(){...}
function format_value(){...}
...
return {
destroy: ...,
update: (new_pattern)=>{
pattern=new_pattern;
format_value();
}
}
}
let zagran = false;
</script>
Телефон: +7 <input type="text" use:format_by_pattern={'(***) ***-**-**'} /> <br/>
Паспорт: <input type="text" use:format_by_pattern={zagran ? 'серия ** №*******' : 'серия **** №******'} /> <br/>
<input type="checkbox" bind:checked={zagran}/> - заграничный
Рассмотрим ближе, что тут происходит. Появился новый чекбокс, который, благодаря двусторонней привязке bind:checked
, устанавливает значение переменной zagran
в true
или false
. При переключении состояния флажка, срабатывает реактивность Svelte и тернарное выражение помещает нужную маску в параметр соответствующей директивы use:format_by_pattern
.
Однако, как мы помним, функция Действия вызывается только лишь при монтировании элемента в DOM-дерево, а <input />
при смене типа паспорта как был в DOM-дереве, так там и остаётся. Изменение значения параметра директивы не приведет к повторному вызову этой функции.
Чтобы обработать изменение параметра, нам нужно добавить в объект, который возвращает функция Действия, еще один метод — update
. Он будет вызываться всякий раз, когда изменится параметр директивы. Новое значение параметра будет передано методу в качестве аргумента.
В этой статье я показал лишь простейшее использование Действий. Но в реальности, они могут быть применены в огромном количестве различных задач. Например, упростить использование внешних библиотек, которые работают с DOM-элементами напрямую — различного рода всплывающие подсказки, автодополнения для полей ввода и прочие. С помощью Действия можно очень элегантно реализовать концепцию порталов в Svelte. Если вы уже используете Svelte на работе или для личных проектов, но до сих пор обходились без Действий, самое время попробовать.
На заметку: Совсем скоро в Москве состоится Svelte Russia Meetup #1. Регистрируйтесь, места ещё есть. Для тех кто не сможет присутствовать лично, будет организована трансляция и запись мероприятия.