Svelte: знакомимся с Действиями

  • Tutorial

Недавно на Хабре появилась статья от @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. Регистрируйтесь, места ещё есть. Для тех кто не сможет присутствовать лично, будет организована трансляция и запись мероприятия.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 2

    0
    Спасибо, все доходчиво и по делу! Согласен, одна из лучших фичей Svelte.

    На самом же деле концепция очень простая — это что-то вроде функции жизненного цикла для любого HTML-элемента.

    Шикарная формулировка! ;-)
      +1

      Всего лишь вольный перевод фразы из учебника — Actions are essentially element-level lifecycle functions.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое