Создаём To-do List на Alpine.js
13-02-2021Время чтения ~ 5 мин.Alpine.js 5362
Наверное все программисты создавали свой 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 свойства?
Наверное можно через
.