OVERFLOW: AUTO;

Aiming to be modern, practical skilled front-end engineer.

Nuxt + TypeScript + GraphCMS でブログサイトを作る_03

f:id:show-hei:20200710032038p:plain

Nuxt.js と TypeScript の導入設定はこちらから。

overflow-auto.hatenablog.com

overflow-auto.hatenablog.com

ブログを作ると言いつつ、Nuxt.js + TypeScript の実装ってどうなるの?ってことを確認するために軽い Todo アプリを作っているところ。

今回は Todo データを保持する Store を作って Todo アプリを作ります。

Todo の型定義

store を追加する前に、todo の型を先に定義してしまいましょう。

~/types/todoTypes.d.ts というファイルを追加し、下記のように記述します。

想定として、1つの Todo のタスクをオブジェクトで表現しようと考えています。Todo のタスクとして記載されるテキストと、そのタスクが完了したかを示す値を含みます。

export interface Todo {
  done: boolean
  text: string
}

export interface TodoState {
  todos: Todo[]
}

interface Todo ... では、一つの Todo タスクに含まれる情報を表しています。

interface TodoState ... では、Todo タスクの集合リストを表しています。

Todo Store の追加

この辺は公式を参考にしつつ、作っていきます。

typescript.nuxtjs.org

まずは ~/store/todo.ts というファイルを作成します。

前回書いた通り、ここで記述した内容は後ほど ~/store/index.ts で呼び出すことになります。

公式での Vanilla Vuex の書き方

nuxt-typed-vuex を使わない素の状態だと以下のようになります。

// 素の vuex 型を使う。
import { GetterTree, ActionTree, MutationTree } from 'vuex'
// 設定した Todo の型を使用するためインポートする。
import { Todo } from '~/types/todoTypes'

export const state = () => ({
  // todos は配列となり、要素は Todo 型を持つ。
  todos: [] as Todo[]
})

// おまじない的に入れてる。
export type RootState = ReturnType<typeof state>

export const getters: GetterTree<RootState, RootState> = {
  todos: state => state.todos
}

export const mutations: MutationTree<RootState> = {
  ADD: (state, todo: Todo) => (state.todos.push(todo)),
  COMPLETE: (state, index: number) => (
    state.todos.done = true
  )
}

export const actions: ActionTree<RootState, RootState> = {
  add({ commit }, todoText: string) {
    const todo: Todo = {
      text: todoText,
      done: false
    }
    commit('ADD', todo)
  },
  complete({ commit }, index: number) {
    commit('COMPLETE', index)
  }
}

ごくごく普通の vuex に型が付いただけって感じですね。

見てもらうと分かるように vuex で定義してある GetterTree, ActionTree, MutationTree を付与する際、それぞれに型情報として RootState を渡す必要があります。

nuxt-typed-vuex での書き方

では nuxt-typed-vuex での書き方を見ていきましょう。

import {
  getterTree,
  mutationTree,
  actionTree
} from 'typed-vuex'
import { Todo } from '~/types/todoTypes'

export const state = () => ({
  todos: [] as Todo[]
})

export const getters = getterTree(state, {
  todos: state => state.todos
})

export const mutations = mutationTree(state, {
  ADD(state, todo: Todo): void {
    state.todos.push(todo)
  },
  COMPLETE(state, index: number) {
    state.todos.done = true
  }
})

export const actions = actionTree({ state, mutations }, {
  // 返却する型を付けておく -> ここではデータを返さないので void 型
  add({ commit }, todoText: string): void {
    const todo: Todo = {
      done: false,
      text: todoText
    }
    commit('ADD', todo)
  },
  complete({ commit }, id: number): void {
    commit('COMPLETE', id)
  }
})

見た目に大きく変わったのは、Vanilla Vuex で GetterTree などに当てていた型情報がなくなったところですね。かなりスッキリしました。

typed-vuex は Vanilla Vuex をラッピングしており、内部でそのへんを処理してくれています。

というわけでこちらを採用して続きをやっていきます。

index.ts でインポート

先に作っておいた store/index.ts で todo の store を読み込みます。

// store/index.ts

import { getAccessorType } from 'typed-vuex'

import * as todo from '~/store/todo' // <- 追加

export const state = () => ({})

export const getters = {}

export const mutations = {}

export const actions = {}

export const accessorType = getAccessorType({
  state,
  getters,
  mutations,
  actions,
  modules: { // <- 追加
    todo,
  },
})

ページで Todo アプリ実装

動きを確認するため、シンプルな見た目で実装します。

そもそもブログを作る前の寄り道なので、見た目にこだわる必要が全くないのです。

<template>
  <div>
    <div>
      <label>
        <input
          type="text"
          v-model="todoText"
        >
      </label>
      <button @click="addTodo">
        add
      </button>
    </div>
    <ul>
      <template v-for="(todo, index) in todos">
        <li
          :key="index"
          :class="{ 'is_done': todo.done }"
        >
          {{ todo.text }}
          <button @click="completeTodo(index)">
            Complete
          </button>
        </li>
      </template>
    </ul>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { Todo } from '~/types/todoTypes'

export default Vue.extend({
  data: () => ({
    todoText: ''
  }),
  computed: {
    todos(): Todo[] {
      return this.$accessor.todo.todos
    }
  },
  methods: {
    addTodo() {
      this.$accessor.todo.add(this.todoText)
      this.todoText = ''
    },
    completeTodo(index: number) {
      this.$accessor.todo.complete(index)
    }
  }
})
</script>

<style scoped>
.is_done {
  text-decoration: line-through;
}
</style>

とてもシンプルな Todo ができました。

f:id:show-hei:20200710031922p:plain