點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Alpine 實作 Todo List

Sam Xiao's Avatar 2024-02-11

Todo List 常用來練習不同前端 Framework,直接在 Hugo 平台使用 Alpine 實現 Todo List。

Version

Hugo 0.121.2
Alpine 3.13.5

Todo List

todolist001

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="js/alpine.js" defer></script>
      {{ $title := "Hugo X Alpine Lab" }}
    <title>{{ $title }}</title>
  </head>
  <body x-data="{
    newTodo: '',
    todos: [],
    filter: () => true,
    get filteredTodos() { return this.todos.filter(this.filter) },
    get active() { return this.todos.filter(x => !x.isCompleted) },
    get completed() { return this.todos.filter(x => x.isCompleted) }
  }">
    <div>
      <input type="text" x-model="newTodo" />
      <button @click="
        if (!newTodo) return

        todos.push({ id: Symbol(), isCompleted: false, todo: newTodo })
        newTodo = ''
      ">Add</button>
    </div>
    <div>
      <ul>
        <template x-for="item in filteredTodos" :key="item.id">
          <li>
            <input type="checkbox" x-model="item.isCompleted" />
            <span x-show="!item.isEdit">
              <span x-text="item.todo"></span>
              <button @click="item.isEdit = true; item.todo_ = item.todo">edit</button>
              <button @click="todos = todos.filter(x => x.id !== item.id)">delete</button>
            </span>
            <span x-show="item.isEdit">
              <input type="text" x-model="item.todo_" />
              <button @click="item.isEdit = false; item.todo = item.todo_">save</button>
              <button @click="item.isEdit = false">cancel</button>
            </span>
          </li>
        </template>
      </ul>
    </div>
    <div>
      <span x-text="active.length"></span> <span>items left</span>
      <button @click="filter = () => true">All</button>
      <button @click="filter = x => !x.isCompleted">Active</button>
      <button @click="filter = x => x.isCompleted">Completed</button>
      <button x-show="completed.length" @click="todos = active">Clear Completed</button>
    </div>
  </body>
</html>

Line 11

newTodo: '',
todos: [],
  • x-data 內宣告 state
  • newTodo state :儲存新輸入的 todo
  • todos state:儲存所有 todo

Line 13

filter: () => true,
get filteredTodos() { return this.todos.filter(this.filter) },
  • x-data 內宣告 state 與 getter
  • filter state:儲存 AllActiveCompleted 所需的 filter function
  • filteredTodos getter:當 todo 改變時,filteredTodos getter 會 reactive 跟著改變,只顯示所有目前 filter state 的 filter function 所指定的 todo

Line 15

get active() { return this.todos.filter(x => !x.isCompleted) },
  • x-data 內宣告 getter
  • active getter:當 todo state 改變時,active getter 會 reactive 跟著改變,只顯示所有 未完成 todo

Line 16

get completed() { return this.todos.filter(x => x.isCompleted) }
  • x-data 內宣告 getter
  • completed getter:當 todo state 改變時,completed getter 會 reactive 跟著改變,只顯示所有 已完成 todo

Line 19

<input type="text" x-model="newTodo" />
  • x-model:將欲輸入的新 todo 以 綁定到 newTodo state

Line 20

<button @click="
  if (!newTodo) return

  todos.push({ id: Symbol(), isCompleted: false, todo: newTodo })
  newTodo = ''
">Add</button>
  • 新增一筆 todo,並直接將 click event handler 的代碼寫在 HTML 內
  • 使用 push 新增 todo array
  • id 使用 Symbol() 實現 UUID

Line 28

<ul>
  <template x-for="item in filteredTodos" :key="item.id">
    <li>
      <span x-text="item.todo"></span>  
    </li>
  </template>
</ul>
  • x-for :列舉 filteredTodos getter
  • x-text:將 data 綁定顯示

x-for 必須寫在 <template>

Line 31

<input type="checkbox" x-model="item.isCompleted" />
  • x-model:將 checkbox 以綁定到 item.isCompleted

Line 32

<span x-show="!item.isEdit">
  <span x-text="item.todo"></span>
  <button @click="item.isEdit = true; item.todo_ = item.todo">edit</button>
  <button @click="todos = todos.filter(x => x.id !== item.id)">delete</button>
</span>
  • x-show:若是 非編輯模式,則直接顯示 todo
  • Button 此時為 editdelete
  • 在 item 下新增 isEdit property 為 true 表示為 編輯模式,此時 button 會改為顯示 savecancel
  • 在 item 下新增 todo_ property 作為臨時 state,並將目前 toto 值寫入 todo_,作為綁定 <input>
  • 使用 filter() 取代 splice() 刪除 array 中的 element

由於 JavaScript 為 動態語言,我們可對 Object 動態新增 property 作為臨時變數,此為 JavaScript 慣用手法

Line 37

<span x-show="item.isEdit">
  <input type="text" x-model="item.todo_" />
  <button @click="item.isEdit = false; item.todo = item.todo_">save</button>
  <button @click="item.isEdit = false">cancel</button>
</span>
  • x-show:若是 編輯模式,則以 <input> 顯示 todo,注意此時 v-model 綁定到 item.todo_ 而非 item.todo,此為儲存 暫時 的 todo,主要為了 cancel 所用
  • Button 此時為 savecancel
  • 將 item 的 isEdit 設定為 false 表示為 非編輯模式,此時 button 會改為顯示 editdelete
  • 因為目前編輯的 <input> 綁定到 item.todo_,將資料從 item.todo_ 複製到 item.todo 完成儲存

Line 47

<span x-text="active.length"></span> <span>items left</span>
  • 顯示目前尚未完成的 todo 筆數

Line 48

<button @click="filter = () => true">All</button>
  • 顯示所有 todo,直接提供 filter function

Line 49

<button @click="filter = x => !x.isCompleted">Active</button>
  • 顯示尚未完成 todo,直接提供 filter function

Line 50

<button @click="filter = x => x.isCompleted">Completed</button>
  • 顯示已完成 todo,直接提供 filter function

Line 51

<button x-show="completed.length" @click="todos = active">Clear Completed</button>
  • x-show:若有已完成 todo,則顯示 Clear Completed button
  • 看起來好像是刪除 completed todo,其實只要將 active todo 指定給 todos state 即可

Conclusion

  • Alpine 其實並不需如 Vue 一樣將 HTML 與 JavaScript 分開,可直接如 Tailwind CSS 寫在 HTML 即可
  • Getter 也算廣義的 data,所以寫在 x-data
  • Alpine 的 x-showx-if 好用,x-if 還要多一層 <template>
  • 若以 FP 風格開發 JavaScript,常常只有一行而已,這種寫在 HTML 剛剛好
  • Alpine = FP + HTML 算 coding style 最高境界,且實作也非常精簡,包含 HTML 僅需 54 行而已