Создаём To-do List на Alpine.js

Наверное все программисты создавали свой 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-вариантов. Во всяком случае я не нашёл. :-)

Комментариев: 2 RSS

1Ян03-03-2021 17:25

Подскажи, возможно ли в alpine у элементов в директории x-for узнать css свойства?

Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)