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;
    }
  };
}