# มือใหม่ Nuxt.js + TypeScript pt.4 SImple Todo List Walkthrough

สวัสดีครับ หลังจากเราได้ทราบ concept การใช้ nuxt.js ร่วมกับ type script กันมาบ้างแล้ว ถึงเวลาที่เราจะเริ่มลองนำมาใช้งานจริงกันครับ\
\
วันนี้เราจะมาทำ Todo List ง่าย ๆ กันครับ

### Todo list ของเราจะทำอะไรได้บ้าง?

1. เพิ่ม todo ได้
2. แก้ไข todo ได้
3. ลบ todo ได้
4. เก็บ todo list ของเราไว้ใน local storage
5. done ให้กับ todo ได้
6. undone ให้กับ todo ได้
7. มีบอร์ดแสดงค่า total todo, total done todo และ total undone todo
8. มี List แสดง todo และสามารถเลือกที่จะแสดงแค่ done todo หรือ undone todo ได้

### Design todo จาก requirement

หลังจากเราได้ requirement มาเราก็ใช้ figma design ออกมาได้ประมาณนี้ครับ

![Simple todolist design by mnagon](https://2538720252-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MRiJZDDMVovajLrsF56%2F-MTMOD8xEL1lGO36C9lR%2F-MTMSXOWciQGw7t8yoDb%2FDesktop.png?alt=media\&token=ee2171be-521c-415e-8c82-a54e10bfa839)

> :warning: ผมขอข้ามขั้นตอนการสร้าง nuxt project และการติดตั้ง module ต่าง ๆ นะครับ หากใครยังไม่ทราบว่าต้องติดตั้งหรือใช้งานอะไรบ้าง แนะนำให้กลับไปอ่านบทก่อนหน้าได้ครับ

### แยก Component

เราจะพิจารณาจาก design ของเราครับว่า จะสามารถแยก component อะไรบ้าง ซึ้งจาก design ที่ผมทำไว้ผมแยก component ออกมาได้แบบนี้ครับ

![Todo list component](https://2538720252-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MRiJZDDMVovajLrsF56%2F-MTMOD8xEL1lGO36C9lR%2F-MTMWNP23Bo-10S_YfQM%2FDesktop%20component.png?alt=media\&token=54e41b33-eff8-4b1f-9bb9-a23e874e3182)

1. Navbar (Navbar)
2. Scoreboard (Board)
3. Todo input box (TodoInput)
4. Todo card (TodoItem)

จะเห็นได้ว่าเราแยกออกมาได้เป็น 4 component ครับ ให้เราทำการสร้างไฟล์ Navbar.vue, Board.vue, TodoInput.vue และสุดท้าย TodoItem.vue ที่ folder components ครับ

ให้เรา style ตัว component ออกมาให้เหมือน design ของเราที่สุดได้เลยครับ ผมไม่ขอลงรายละเอียดของ template และ style นะครับ เดียวมันจะเยอะเกิน แต่ว่าหากใครที่ขี้เกียจเขียน template และ style เอง ผมมี project starter ที่ผมทำเสร็จแล้วมาแจกครับเป็นตัวที่มีแค่ส่วนของ template และ style พร้อมที่จะเข้าสู่การเขียน script เลยครับ

Download project stater >> <https://github.com/mnagon/nuxt-typescript-todolist-starter>

### เริ่มจาก Store กันก่อน

ให้เราติดตั้ง module nuxt-typed-vuex ให้เรียบร้อยก่อนนะครับ หลังจากนั้นสร้างไฟล์ index.ts ใน folder store กันเลยครับ\\

#### State

State ของ todo list ก็ไม่มีอะไรมากเลยครับก็มีแค่ array ของ todo เท่านั้นเองครับ ส่วน todo ของเราก็เป็น object ที่มี string และ boolean อย่างหละตัวเพื่อเก็บ todo text และ สถานะว่า done หรือ undone เราก็จะเริ่มจากการสร้าง interface ให้ todo และ state ได้ออกมาแบบนี้ครับ

```javascript
//store/index.ts
interface Todo {
  done: boolean
  todo: string
}

interface State {
  todoList: Todo[]
}

export const state = (): State => ({
  todoList: [],
})
```

#### Getters

ส่วนของ getters เราจะทำ  getters ไว้สองตัวครับคือ doneTodo และ undoneTodo เพื่อเวลาเรา filter done หรือ undone todo เราสามารถมาเอา todo list ได้จาก getters สองตัวนี้ครับ (\* อย่าลืม import getterTree ด้วยนะครับ)

```javascript
//store/index.ts
import { getterTree } from 'nuxt-typed-vuex'

...

export const getters = getterTree(state, {
  doneTodo: (state: State): Todo[] =>
    state.todoList.filter((todo) => todo.done),
  undoneTodo: (state: State): Todo[] =>
    state.todoList.filter((todo) => !todo.done),
})
```

#### Mutations

เราจะทำ store ของเราให้ครบในส่วนของ CRUD เลยครับ เพียงแต่ว่า read เราใช้ส่วนของ state และ getters ไปแล้ว เพราะฉนั้น mutations ของเราต้องมี create, update และ delete ครับ เหมือนตัวอย่างด้านล่างเลย (\*อย่าลืม import mutationTree ด้วยนะครับ)

```javascript
//store/index.ts
import { getterTree, mutationTree } from 'nuxt-typed-vuex'

...

interface UpdateTodoPayload {
  todo: Todo
  todoIndex: number
}

...

export const mutations = mutationTree(state, {
  UNSHIFT_TODO_LIST(state, todo: Todo): void {
    state.todoList.unshift(todo)
  },
  UPDATE_TODO(state, { todo, todoIndex }: UpdateTodoPayload): void {
    state.todoList[todoIndex].todo = todo.todo
    state.todoList[todoIndex].done = todo.done
  },
  SPLICE_TODO_LIST(state, index: number): void {
    state.todoList.splice(index, 1)
  }
})
```

จะเห็นได้ว่าในส่วนของ UPDATE\_TODO ของผม ผมต้องการ payload ที่เป็น object ที่มี todo และ index ของ todo ที่ต้องการอัปเดท ซึ้งเราก็ควรเขียน type ให้กับ payload ตัวนี้ด้วย เพื่อป้องกันการส่งผ่านค่า payload ที่ผิดพลาดเข้ามาได้ครับ ผมจึงแอบไปสร้าง interface สำหรับ UpdateTodoPayload เข้ามาเหมือนในตัวอย่างครับ

#### Actions

สำหรับ actions ช่วงแรก็ไม่มีอะไรมากครับ ทำ actions ให้ครอบคลุมทั้ง mutations ของเราไปก่อนเหมือนตัวอย่างด่านล้างครับ (\* อย่าลืม import actionTree นะครับ)

```javascript
//store/index.ts
import { getterTree, mutationTree, actionTree } from 'nuxt-typed-vuex'

...

export const actions = actionTree(
  { state, getters, mutations },
  {
    addTodo({ commit, dispatch }, todo: string): void {
      commit('UNSHIFT_TODO_LIST', { todo, done: false })
    },
    editTodo({ commit, dispatch }, { todo, todoIndex }: UpdateTodoPayload): void {
      commit('UPDATE_TODO', { todo, todoIndex })
    },
    deleteTodo({ commit, dispatch }, index: number): void {
      commit('SPLICE_TODO_LIST', index)
    }
  }
)
```

#### อย่าลืมประกาศ $accessor

สุดท้าย export accessorType และ อย่าลืมสร้างไฟล์ index.d.ts และประกาศ $accessor ด้วยครับ ใครที่ไม่รู้ว่าคืออะไรสามารถย้อนกลับไปดูที่ pt.3 ได้ครับ

#### ทำงานกับ Localstorage

เพื่อเก็บ todo list ของเราไว้ใน local storage เราจะมาอัปเดท store ของเราให้ทำงานร่วมกับ localstorage กันครับ โดยผมได้สร้าง action ชื่อว่า updateStorage เพื่อเก็บ todo list ของเราไว้ใน localstorage แล้วหลังจากนั้นผมจะเรียกใช้ action นี้ทุกครั้งที่มีการ create, update หรือ delete todo เหมือนตัวอย่างด้านล่างเลยครับ

```javascript
//store/index.ts

...

export const actions = actionTree(
  { state, getters, mutations },
  {
    addTodo({ commit, dispatch }, todo: string): void {
      commit('UNSHIFT_TODO_LIST', { todo, done: false })
      dispatch('updateStorage')
    },
    editTodo(
      { commit, dispatch },
      { todo, todoIndex }: UpdateTodoPayload
    ): void {
      commit('UPDATE_TODO', { todo, todoIndex })
      dispatch('updateStorage')
    },
    deleteTodo({ commit, dispatch }, index: number): void {
      commit('SPLICE_TODO_LIST', index)
      dispatch('updateStorage')
    },
    updateStorage({ state }): void {
      localStorage.setItem('todoList', JSON.stringify(state.todoList))
    },
  }
)

...

```

ไม่เพียงเท่านั้นครับ เราก็ต้องมีส่วนของการ get localstorage มาใช้ด้วย เราก็จะเพิ่ม action และ mutation เหมือนตัวอย่างนี้เลยครับ

```javascript
//store/index.ts

...

export const mutations = mutationTree(state, {
  SET_TODO_LIST(state, todoList: Todo[]): void {
    state.todoList = todoList
  },
  
  ...
  
})

export const actions = actionTree(
  { state, getters, mutations },
  {
    getTodoList({ commit }): void {
      const todolist: Todo[] = JSON.parse(
        localStorage.getItem('todoList') || '[]'
      )
      commit('SET_TODO_LIST', todolist)
    },
    
    ...
    
  }
)
```

#### Set current tab?

จาก design จะเห็นได้ว่าเรามี navbar ที่จะเลือกโชว์ todo list ในแบบต่าง ๆ ด้วย ในขณะที่เรากระทำกับ navbar แต่ส่งผลมาถึง todo item list ข้าม componet แบบนี้ ผมจึงเอามาจัดการใน store ด้วยครับ โดยเพิ่ม current tab เข้าไปใน state แล้วเพิ่ม mutation และ action จัดการเอาไว้ด้วย เหมือนตัวอย่างนี้ครับ

```javascript
//store/index.ts

...

interface State {
  currentTab: string
  todoList: Todo[]
}

export const state = (): State => ({
  currentTab: 'all',
  todoList: [],
})

export const mutations = mutationTree(state, {

  ...

  SET_CURRENT_TAB(state, tab: string): void {
    state.currentTab = tab
  },
})

export const actions = actionTree(
  { state, getters, mutations },
  {
  
    ...
  
    setTab({ commit }, tab: string): void {
      commit('SET_CURRENT_TAB', tab)
    },
  }
)
```

เพียงเท่านี้ก็เสร็จสิ้นการจัดเตรียม store ของเราในรูปแบบ typescript แล้วครับ

### จัดการกับ Component

ต่อไปเราจะมาใช้งาน store ของเราในแต่หละ component ครับ โดยจะเรียงตามลำดับกันเลยครับ

#### Navbar.vue

Navbar ของเราไม่มีอะไรมากครับ เพียงแค่เรากดตรง tab ไหนเราก็เรียกใช้ action setTab นั้นและเราก็เพิ่ม class active ให้กับ tab นั้นเมื่อ currenTab ตรงกับ ตัวเอง เหมือนตัวอย่างนี้ครับ

```javascript
//components/Navbar.vue
<template>

  ...
  
  <ul class="nav-list">
    <li
      :class="{ active: $accessor.currentTab === 'all' }"
      @click="$accessor.setTab('all')"
    >
      All
    </li>
    <li
      :class="{ active: $accessor.currentTab === 'undone' }"
      @click="$accessor.setTab('undone')"
    >
      Todo
    </li>
    <li
      :class="{ active: $accessor.currentTab === 'done' }"
      @click="$accessor.setTab('done')"
    >
      Done
    </li>
  </ul>
  
  ...
  
</template>
```

#### Board.vue

ส่วนของ Board ยิ่งง่ายไปกันใหญ่ครับ เพียงแค่เราเข้าถึง state หรือ getters นั้น ๆ แล้ว .length ออกมาเพื่อเป็นค่า totol แค่นั้นเองครับ ไปดูตัวอย่างกันเลยครับ

```javascript
//component/Board.vue
<template>
  <div class="board">
    <div class="counter">
      <span class="counter__total">{{ $accessor.todoList.length }}</span>
      <span class="counter__label">TOTAL</span>
    </div>
    <div class="counter">
      <span class="counter__total">{{ $accessor.undoneTodo.length }}</span>
      <span class="counter__label">TODO</span>
    </div>
    <div class="counter">
      <span class="counter__total">{{ $accessor.doneTodo.length }}</span>
      <span class="counter__label">DONE</span>
    </div>
  </div>
</template>
```

#### TodoInput.vue

ในส่วนของ Input ก็ไม่มีอะไรซับซ้อนมากครับ เพียงเรามี input หนึ่งตัว และหลังจากกดปุ่ม add หรือ enter ก็ให้เรียกใช้ method addTodo เพื่อส่งค่าที่ป้อนไปกับ actions addTodo อีกที แค่นี้เองครับ ลองไปดูตัวอย่างกันเลย

```javascript
//component/TodoInput.vue
<template>
  <div class="todo-input">
    <input
      v-model="newTodo"
      type="text"
      placeholder="Type todo here..."
      @keyup.enter="addTodo"
    />
    <button class="add-button" @click="addTodo">
      <i class="fas fa-plus"></i>
    </button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

interface State {
  newTodo: string
}
export default Vue.extend({
  name: 'TodoInput',
  data(): State {
    return {
      newTodo: '',
    }
  },
  methods: {
    addTodo(): void {
      if (this.newTodo) {
        this.$accessor.addTodo(this.newTodo)
        this.newTodo = ''
      }
    },
  },
})
</script>
```

#### Index.vue

ก่อนที่เราจะเริ่มในส่วนของ TodoItem.vue เราต้องมาจัดการข้อมูล todo list ของเราในหน้า index ก่อนครับ ทุกครั้งที่เริ่มโหลดหน้า Index ขึ้นมาเราต้อง get todo มาจาก localstorage เพื่อเซ็ตให้กับ store ของเราก่อน อีกทั้งเรายังต้องแสดง todo list ให้สอดคล้องกับ tab ใน navbar ที่เราได้เลือกไว้อีกด้วยครับ อีกทั้งหน้า index นี้แหละที่เราจะประกอบ Navbar, Board, TodoInput และ TodoItem เข้าด้วยกัน เรามาดูตัวอย่างด้านล่างกันเลยครับ

```javascript
//pages/index.vue
<template>
  <div class="container">
    <navbar />

    <div class="main">
      <board />
      <div class="todo">
        <todo-input />
        <div class="todo-wrapper">
          <todo-item
            v-for="(todo, $todoIndex) in todoList"
            :key="todo + $todoIndex"
            :todo="todo"
            :index="$todoIndex"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Navbar from '~/components/Navbar.vue'
import Board from '~/components/Board.vue'
import TodoItem from '~/components/TodoItem.vue'
import TodoInput from '~/components/TodoInput.vue'

interface Todo {
  done: boolean
  todo: string
}

interface State {}

export default Vue.extend({
  components: {
    Navbar,
    Board,
    TodoItem,
    TodoInput,
  },
  data(): State {
    return {}
  },
  computed: {
    todoList(): Todo[] {
      return this.$accessor.currentTab === 'all'
        ? this.$accessor.todoList
        : this.$accessor.currentTab === 'done'
        ? this.$accessor.doneTodo
        : this.$accessor.currentTab === 'undone'
        ? this.$accessor.undoneTodo
        : []
    },
  },
  mounted(): void {
    this.$accessor.getTodoList()
  },
})
</script>

```

จะเห็นได้ว่า todoList ที่ผมนำมาวนลูป todo-item นั้นผมได้ประกาศเป็น computed ที่ return ค่าให้สอดคล้องกับ current tab นั้นเองครับ (\*สำหรับใครที่งงโค้ดตรงนี้นะครับ มันเป็น if-else ธรรมดาๆ เลยครับเพียงแค่ผมเขียนในรูปแบบ shorthand แค่นั้นเอง)

#### TodoItem.vue

มาถึง component สุดท้ายของเรานะครับ ตัวนี้พิเศษหน่อยเพราะว่าการ set done, edit todo และ delete todo จะอยู่ที่ component นี้หมดเลย อาจมีลูกเล่นในการทำ edit mode และมีการเปลี่ยนแปลง style จากสถานะ done หรือ undone อยู่ด้วย แต่ action ส่วนใหญ่เราได้สร้างและเตรียมไว้ใน store เรียบร้อยแล้ว ก็แค่จับมาใช้งานให้ครบเท่านั้นเองครับ มาดูโค้ดตัวอย่างกันดีกว่า

```javascript
//components/TodoItem.vue
<template>
  <div
    class="todo-item"
    :class="{
      done: todo.done,
      editing,
    }"
  >
    <button class="delete-button" @click="deleteTodo">
      <i class="fas fa-trash-alt"></i>
    </button>
    <button class="edit-button" @click="setEditing(true)">
      <i class="fas fa-pencil-alt"></i>
    </button>
    <div class="todo-card">
      <span class="todo-status">
        {{ todo.done ? 'Done' : 'Undone' }}
      </span>
      <div class="todo-status-icon" @click="toggleDone">
        <img v-if="todo.done" src="~/assets/check.svg" />
      </div>
      <div v-if="!editing" class="todo-text" @click="setEditing(true)">
        {{ todo.todo }}
      </div>
      <input
        v-else
        :id="id"
        v-model.lazy="todoForm"
        class="todo-edit-input"
        type="text"
        @blur="setEditing(false)"
        @keyup.enter="setEditing(false)"
      />
    </div>
  </div>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue'

interface State {
  editing: boolean
  id: string
}

interface Todo {
  done: boolean
  todo: string
}

export default Vue.extend({
  props: {
    index: {
      type: Number,
      required: true,
    },
    todo: {
      type: Object as PropType<Todo>,
      required: true,
    },
  },
  data(): State {
    return {
      id: 'input' + this.index,
      editing: false,
    }
  },
  computed: {
    todoForm: {
      get(): string {
        return this.todo.todo
      },
      set(val: string): void {
        const todo: Todo = { done: this.todo.done, todo: val }
        this.$accessor.editTodo({ todo, todoIndex: this.index })
      },
    },
  },

  methods: {
    setEditing(bool: boolean): void {
      this.editing = bool
      if (bool) {
        this.$nextTick(() => {
          document.getElementById(this.id)?.focus()
        })
      }
    },
    deleteTodo(): void {
      this.$accessor.deleteTodo(this.index)
    },
    toggleDone(): void {
      const todo: Todo = { done: !this.todo.done, todo: this.todo.todo }
      this.$accessor.editTodo({ todo, todoIndex: this.index })
    },
  },
})
</script>
```

เพียงเท่านี้ก็เสร็จเรียบร้อยแล้วครับสำหรับ simple todo list ของเรา :tada:&#x20;

ซึ้งใครอยากเห็นตัวเต็มของโค้ดที่สำเร็จแล้วทั้งหมดสามารถดูได้จาก repo นี้เลยครับ <https://github.com/mnagon/nuxt-typescript-todolist>

หลังจากที่เราได้เข้าใจ concept ของ nuxt + typescript จวบจนนำมาสร้าง project เล็ก ๆ กันได้เรียบร้อยแล้วผมก็ขอจบ series nuxt.js + typescript ไว้แค่เพียงเท่านี้ครับ การใช้งานที่ advance หรือ trip trick ผมจะทำอีกบทไว้โดยเฉพาะ และจะอัปเดทเรื่อย ๆ อีกทีครับ \
\
สุดท้ายแล้วหากมีข้อผิดพลาดประการใดขออภัยมา ณ ที่นี้ด้วยนะครับ ขอบคุณที่อ่านกันจนจบ series นะครับ :pray:&#x20;
