Создаём To-do List на Alpine.js
13-02-2021Reading time ~ 5 min.Alpine.js 4381
Наверное все программисты создавали свой todo-«велисипед», мне же было интересно разобраться насколько сложно сделать его с помощью Альпины. Для затравки, если будет время, можете посмотреть похожие примеры, только сделанные на других js-фреймворках — todomvc.com.
Когда я начал делать на Alpine, то сразу же сделал ошибку, что использовал «классический» подход в реализации. В итоге код получился запутанным и сложным. Но потом всё-таки сообразил, что можно сделать намного проще и элегантней. В «обычном» варианте нужно думать как связывать реальные данные с их хранением, то с Альпиной это работает из «коробки». По сути мы и работаем только с массивом данных, а отображение строится автоматом.
Мой пример достаточно простой. Я не стал его усложнять, оставил только базовый функционал. Так что при желании, вы сможете легко добавить что вам нужно.
Общее описание
В качестве хранилища используется самый обычный localStorage. То есть данные сохранятся в браузере даже после его закрытия.
Вверху поле, где нужно ввести текст и нажать Enter. После этого текст попадает в общий список внизу. Выводится текст, дата и две кнопки: одна для удаления записи, другая для отметки «выполнено» (текст зачёркивается).
Под списком находится кнопка «Clear all» — удалить всё.
Вот рабочий пример, можете побаловаться. :-)
Полный код я приведу в конце статьи.
Основной контейнер
Здесь мы задаём компонент Альпины, а также создаём 3 переменные:
todos
— хранит список элементовtnew
— значение поля ввода для нового текстаlsname
— имя хранилища localStorage
<div x-data="{todos: [], tnew: '', lsname: 'myTodo1'}" x-init="() => { let ls = JSON.parse(localStorage.getItem(lsname)) || []; if (ls) todos = ls; $watch('todos', value => { localStorage.setItem(lsname, JSON.stringify(value)); }); }" class="layout-center-tablet mar30-tb">
Директива x-init
сдержит колбэк-функцию, которая сработает в момент инициализации компонента.
- получаем текущий список из localStorage
- если он есть, то присваиваем его переменной
ls
После этого вешаем слушателя (через $watch
) на переменную todos
. Как только она изменится, то сработает колбэк, который обновит данные в localStorage новым значением.
В чём «фишка»? Нам не нужно где-то ещё даже задумываться о том, как и где хранить данные. Есть массив todos
, который понимает компонент, а связь его с localStorage реализуется через $watch
. То есть к localStorage мы больше не прикасаемся.
CSS-разметка выполнена традиционно на Berry CSS. Если вы уже используете этот фреймворк, то и хорошо. Нет — зря, многое теряете...
Блок нового текста
Здесь используется самый обычный текстовый INPUT, который связывается через x-model
с переменной tnew
.
<div class="pad20 bg-blue200 mar30-b"> <input x-model="tnew" x-on:keydown.enter.debounce=" if (tnew) { let d = new Date(); todos.push({text: tnew, completed: false, date: d.toLocaleString()}); tnew = ''; }" class="form-input w100" placeholder="enter a new task and press ENTER..."> </div>
Чтобы добавить текст в список todos
мы цепляемся к событию keydown.enter
. Параметр debounce
нужен для исключения ложных срабатываний. По этому событию мы проверяем, что текст не равен пустой строке, после этого в todos
добавляем новый элемент массива, который представляет собой объект с полями text
, completed
, date
.
Эти данные нам нужны для вывода всех элементов массива.
Вывод элементов списка
Это самый объёмный участок кода. Поскольку мы выводим todos
, то делаем это в цикле x-for
и используем TEMPLATE.
<template x-for="(todo, index) in todos"> <div x-show.transition.duration="todo" class="mar5-tb bor1 bor-solid-b bor-gray300"> <div class="flex"> <span x-text="todo.text" class="t150 flex-grow5" :class="{'t-strike t-gray400': todo.completed}"></span> <span @click="todos[index] = {text: todo.text, completed: !todo.completed, date: todo.date}" class="t120 t-gray600 hover-t-green600 pad30-r cursor-pointer">✔</span> <span @click="todos = todos.filter((item, key) => key !== index)" class="t120 t-gray600 hover-t-red600 cursor-pointer">✘</span> </div> <div x-text="todo.date" class="t90 t-gray600 mar5-t"></div> </div> </template>
Первый SPAN выводит текст из через x-text
. С помощью css-классов t-strike t-gray400
делаем его зачеркнутым и бледным. Здесь работает связка :class
с todo.completed
.
Блок даты выводится просто как есть (todo.date
). А вот кнопки устроены немного сложнее.
Первая ✔-кнопка переписывает текущий элемент todos[index]
, где меняет свойство completed
. Это позволяет инвертировать пункт как угодно.
Вторая ✘-кнопка удаляет текущий пункт (а это мы определяем по index
) в todos
. К сожалению в JS нет нормального метода для удаления элемента массива (как например в PHP), поэтому приходится формировать новый массив через filter
, а потом его присваивать себе же. То есть этот js-код можно трактовать так: удалить из массива элемент с указанным индексом.
Для красивой анимации используется x-show.transition.duration="todo"
— здесь хитрость в том, что как только элемент будет удалён (это не что иное, как todo
), то блок красиво скроется.
Кнопка удаления
Кнопка показывается только если есть какие-то элементы, а по её клику просто очищаем массив todos
. Поскольку у нас реактивность Альпины, то всё что нужно автоматом обновится как нужно.
<button x-show="todos.length" @click="todos = []" x-cloak class="button button1 mar20-t">Clear all</button>
Полный код
Полный код, если вы хотите его использовать у себя.
<div x-data="{todos: [], tnew: '', lsname: 'myTodo1'}" x-init="() => { let ls = JSON.parse(localStorage.getItem(lsname)) || []; if (ls) todos = ls; $watch('todos', value => { localStorage.setItem(lsname, JSON.stringify(value)); }); }" class="layout-center-tablet mar30-tb"> <div class="pad20 bg-blue200 mar30-b"> <input x-model="tnew" x-on:keydown.enter.debounce=" if (tnew) { let d = new Date(); todos.push({text: tnew, completed: false, date: d.toLocaleString()}); tnew = ''; }" class="form-input w100" placeholder="enter a new task and press ENTER..."> </div> <template x-for="(todo, index) in todos"> <div x-show.transition.duration="todo" class="mar5-tb bor1 bor-solid-b bor-gray300"> <div class="flex"> <span x-text="todo.text" class="t150 flex-grow5" :class="{'t-strike t-gray400': todo.completed}"></span> <span @click="todos[index] = {text: todo.text, completed: !todo.completed, date: todo.date}" class="t120 t-gray600 hover-t-green600 pad30-r cursor-pointer">✔</span> <span @click="todos = todos.filter((item, key) => key !== index)" class="t120 t-gray600 hover-t-red600 cursor-pointer">✘</span> </div> <div x-text="todo.date" class="t90 t-gray600 mar5-t"></div> </div> </template> <button x-show="todos.length" @click="todos = []" x-cloak class="button button1 mar20-t">Clear all</button> </div>
Итого
Сам по себе код небольшой, но поскольку завязан на Alpine.js, имеет некоторые «хитрости» в алгоритме. Если же сравнивать с другими реализациями, то наверное это самый компактный вариант среди всех js-вариантов. Во всяком случае я не нашёл. :-)
Подскажи, возможно ли в alpine у элементов в директории x-for узнать css свойства?
Наверное можно через
.