JSFiddle - React, Tailwind, and code Playground
by Patrick Curl
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AlpineJS Todo List</title>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="/styles.css" />
<script
src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"
defer
></script>
</head>
<body>
<main class="main" x-data="todos()" x-init="init()">
<h1 class="title">todos</h1>
<div class="input-group mb-3">
<div
class="input-group-prepend"
x-show.transition.duration.500ms="todos.length"
>
<div class="input-group-text">
<input type="checkbox" x-model="isCompleteAll" />
</div>
</div>
<input
type="text"
class="form-control"
:class="{ 'is-empty': !todos.length }"
x-model="task"
@keydown.enter="addTask()"
/>
<div
class="input-group-append"
x-show.transition.duration.500ms="todos.length"
>
<button class="btn btn-danger" type="button" @click="reset()">
X
</button>
</div>
</div>
<template x-for="(todo, index) in filteredTodos" :key="index">
<li
class="input-group mb-3"
:key="index"
:class="{ 'is-complete': todo.isComplete === true, 'is-editing': todo.isEditing === true, }"
>
<div class="input-group-prepend">
<div class="input-group-text">
<input type="checkbox" x-model="todo.isComplete" />
</div>
</div>
<input
type="text"
class="form-control task-input"
...
CSS
.main {
max-width: 400px;
padding: 200px 15px;
margin-left: auto;
margin-right: auto;
}
.title {
text-align: center;
}
.task {
display: flex;
align-items: center;
flex: 1 0 auto;
margin-bottom: 0;
padding-left: 10px;
}
.is-empty {
border-radius: 0.25rem !important;
}
.is-complete .task {
text-decoration: line-through;
}
.is-editing .task {
color: red;
}
.outro {
display: flex;
justify-content: space-between;
}
.outro .alert {
margin-bottom: 0;
}
JavaScript
function todos() {
const initialState = {
task: "",
todos: [],
filteredTodos: [],
isCompleteAll: false
};
return {
...initialState,
init() {
this.$watch("isCompleteAll", isComplete => {
this.todos.forEach(todo => {
todo.isComplete = isComplete;
});
});
this.$watch("todos", value => {
console.log(value.map(item => ({ ...item })));
this.filteredTodos = this.todos;
});
},
addTask() {
if (this.task.trim() === "") return;
this.todos = [
...this.todos,
{
task: this.task,
isComplete: false,
isEditing: false
}
];
this.task = "";
},
setIsEditingTask(index, isEditing) {
if (this.todos[index].isEditing === isEditing) return;
this.todos[index].isEditing = isEditing;
this.todos = [...this.todos];
if (isEditing) setTimeout(() => this.$refs[index].focus(), 100);
},
removeTask(index) {
this.todos = this.todos.filter((_, _index) => _index !== index);
},
setFilter(type) {
switch (type) {
case 0:
this.filteredTodos = this.todos;
break;
case 1:
this.filteredTodos = this.todos.filter(item => !item.isComplete);
break;
case 2:
this.filteredTodos = this.todos.filter(item => item.isComplete);
break;
}
},
reset() {
this.task = initialState.task;
this.todos = initialState.todos;
this.filteredTodos = initialState.filteredTodos;
this.isCompleteAll = initialState.isCompleteAll;
}
};
}