Alpine.js – события и глобальное хранилище данных

от автора

обложка статьи

В прошлый раз, когда мы делали to-do на Alpine.js, меня очень сильно расстроило, что, хоть я и могу создавать вложенные компоненты, я не могу получать данные из родителя. Через какую-нибудь переменную, $parent, например.

Поэтому мне пришлось запихивать все яица в одну корзину. Свойства и методы, отвечающие за добавление новых задач, перемешались со всеми остальными. Я хотел выделить отдельный компонент, но необходимость доступа к массиву todos меня ограничивала.

Если вы подумали, что это не очень хорошо, то вы не правы. На самом деле, это ужасно.

Всё, расходимся? Нет. Я еще раз полисал документацию и вспомнил про магическое свойство $dispatch. Ну, конечно… однопоточная связь, проброс событий. Ну давайте попробуем. А потом еще переосмыслим всё с глобальным store.

Проброс событий

Для начала откроем наш код.

Первое, что надо сделать, – превратить обертку над input в компонент и перенести в него inputValue.

<div x-data="{ inputValue: '' }" class="add-todo">   <input type="text" x-model="inputValue" placeholder="Новая задача" />   <button @click="addTodo()">Добавить</button> </div>

Естественно, наш новый компонент понятия не имеет, что такое addTodo(). Ему и не надо. Вместо этого мы будем диспатчить CustomEvent.

<button @click="$dispatch('add', inputValue)">Добавить</button>

Мы диспатчим событие add и передаем ему значение inputValue как payload (в спецификации он называется detail).

Теперь это событие надо принять. Примет его наш корневой компонент. И вызовет addTodo() с нашим inputValue.

<div x-data="todos()" x-init="fetchTodos()" @add="addTodo($event.detail)" class="app">   ... </div>

Осталось немного поправить addTodo() и готово.

addTodo: function (inputValue) {   if (!inputValue) {     return;   }    this.todos.push({     id: Date.now(),     title: inputValue,     completed: false,   }); }

Всё круто, вот только теперь inputValue не отчищается. В этой функции, естественно, мы это сделать не можем. Это нужно делать внутри компонента.

<button @click="$dispatch('add', inputValue); inputValue = ''">Добавить</button>

А можно?.. Нет. $dispatch доступен только в разметке. В принципе, так тоже терпимо. Если бы нам нужно было делать больше логики, мы могли бы вместо inputValue = '' вызвать конкретную функцию, которую бы определили в <script>.

У нас получилось перенести inputValue в отдельный компонент. Неплохо, но хотелось бы и addTodo() перенести. Как сделать это?

На самом деле, элементарно. Просто передайте в detail вместо inputValue уже готовый объект to-do и запушьте его в todos.

...

А знаете что? Попробуйте сами. Сделаем статью более обучающей 🙂 И не забудьте отчистить inputValue после. Вот как я это сделал.

Глобальное хранилище данных

Вы, наверное, заметили, что в заголовке заявлена еще одна тема. Всё, что мы делали выше, – это, конечно, круто. Но масштабируемость сильно хромает. И рано или поздно придет мысль: "Было бы круто все данные хранить в отдельном store, как это делает Redux/Mobx/Vuex и т.п. И обращаться уже к нему, не прокидывая ничего вверх без надобности."

Знакомьтесь, Spruce. 2 килобайта чистой годноты.

Вернемся к оригинальному коду и сделаем все по новому. Для начала подключим, а разберемся по ходу.

<head>   ...   <script src="https://cdn.jsdelivr.net/gh/ryangjchandler/spruce@0.6.0/dist/spruce.umd.js"></script>   <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.3.5/dist/alpine.min.js"></script> </head>

До Alpine.js и удалить у Alpine defer. Такую цену придется заплатить. Довольно дешево, я беру.

Теперь подписываем наш корневой компонент к store. Данные внутри компонента нам больше не понадобятся.

<div x-data x-subscribe class="app">   ... </div>

Spruce дает нам одноименную переменную в window. Чтобы создать store, используется Spruce.store(<название>, <объект>).

Spruce.store('data', {   todos: [],   inputValue: '', });

*store может быть неограниченное количество.

Теперь, чтобы дотянутся до значений, можно использовать $store.data.todos.

Сделаем разделение по Vue Options API – данные будем хранить в data, а методы – в methods.

Spruce.store('methods', {   toggleTodo: (id) => {     const todo = $store.data.todos.find((todo) => todo.id === id);     if (todo != null) {       todo.completed = !todo.completed;     }   },   addTodo: function () {     if (!$store.data.inputValue) {       return;     }     $store.data.todos.push({       id: Date.now(),       title: $store.data.inputValue,       completed: false,     });     $store.data.inputValue = '';   },   deleteTodo: function (id) {     $store.data.todos = $store.data.todos.filter((todo) => todo.id !== id);   }, });

Слышите этот звук? Это скрипят зубы у евангелистов иммутабельности. Здесь её нет, она и не нужна, так как масштабы не те. Иммутабельность сложнее, требует больше написанного кода и очень редко когда приносит реальную пользу. Сложность точно не уровня Alpine.

Ну и, собственно, наш template.

<div x-data x-subscribe class="app">   <h1>Планы на сегодня:</h1>   <ul>     <template x-for="todo in $store.data.todos" :key="todo.id">       <li @click="$store.methods.toggleTodo(todo.id)" :class="{'completed': todo.completed}">         <span x-text="todo.title" class="title"></span>         <span @click="$store.methods.deleteTodo(todo.id)" class="delete-todo">&times;</span>       </li>     </template>   </ul>   <div class="add-todo">     <input type="text" x-model="$store.data.inputValue" placeholder="Новая задача" />     <button @click="$store.methods.addTodo()">Добавить</button>   </div> </div>

Последний штрих – нужно получить данные с API. В Spruce для этого есть удобный метод. Если Spruce.on(<событие>, <колбэк>) предназначен для навешивания событий, то Spruce.once(<событие>, <колбэк>) как раз для выполнения какого-то действия один раз. Событие init – то, что мы ищем.

Spruce.once('init', async ({ store }) => {   const response = await fetch('https://jsonplaceholder.typicode.com/todos');   const data = await response.json();   store.data.todos = data.slice(0, 10); });

Mission accomplished.

Вот полный код.

Можно легко превратить наш Options API в "Composition API", где каждый store отвечает за конкретную функциональность. Делите ваши данные, как заблагорассудится.

При этом нам не нужны внутренние компоненты и проброс данных через события, так как все компоненты имеют равный доступ к $store. Актуально на фоне борьбы с архитектурами master/slave 🙂

Полезные ссылки:

*Photo by Jakub Kapusnak on Unsplash

ссылка на оригинал статьи https://habr.com/ru/post/505954/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *