taisablog.com

taisa's engineer blog

Vue.js+TypeScriptで外部APIを使ったTODOリストを作ってみた

Vue.jsで外部APIを使ったTODOリストを作ってみた に続き、それのTypeScript版を作ってみました。TODOリスト用のAPIは以前書いたこちらのAPIGo言語 GORM+GinでTODOリストのAPIを作ってみた」を利用します。CORSを全て許可しているのでどこからでも叩けるようになっています。TypeScriptを書くのは今回が初めてなので、誤っている箇所やもっとよい書き方などがあれば指摘して頂ければと思います。

できたもの

できたものはこちらです。
http://vuejs-ts.taisablog.com/todo

<div class="graybox">
<div class="clearfix"><figure><a href="http://52.196.213.4/wp-content/uploads/2019/08/063dff08aea69d0ee7c6cea19de29030.png"><img class="alignleft size-full wp-image-1684" src="http://52.196.213.4/wp-content/uploads/2019/08/063dff08aea69d0ee7c6cea19de29030.png" alt="" width="600"></a></figure></div>
</div>

APIのエンドポイント

APIのエンドポイントは以下としました。

URL    http://gin.taisablog.com/api/v1/
GET    /todo       // 一覧表示
POST   /todo       // 新規作成
GET    /todo/:id   // 編集画面表示
PUT    /todo/:id   // 編集(今回未使用)
DELETE /todo/:id   // 削除

プロジェクト作成

vue-cliを使ってプロジェクト作成をしました。プロジェクト作成のコマンドを打つと、色々と聞かれますが、TypeScriptを利用する為にManually select featuresを選択し、TypeScriptをONにします。ここではRouterもONにしました。

% npm install -g @vue/cli
% vue create my-project
  default (babel, eslint)
❯ Manually select features

vue-cliでできたプロジェクトのsrc配下の構成は以下となっています。今回はそこにTodo.vueTodoList.vueを追加して実装しました。views配下で実装するだけでも大丈夫ですが、今回はあえてviews/Todo.vueからTodoList.vueコンポーネントを呼び出す形としました。

.
├── App.vue
├── assets
│   └── logo.png
├── components
│   ├── HelloWorld.vue
│   └── TodoList.vue ← 新規追加
├── main.ts
├── router.ts
├── shims-tsx.d.ts
├── shims-vue.d.ts
└── views
    ├── About.vue
    ├── Home.vue
    └── Todo.vue ← 新規追加

views/Todo.vue

TodoList.vueコンポーネントを呼び出します。

<div><figure><img src="../assets/logo.png" alt="Vue logo"></figure></div>
  import { Component, Vue } from 'vue-property-decorator'
  import TodoList from '@/components/TodoList.vue'
  @Component({
    components: {
      TodoList,
    },
  })
  export default class Todo extends Vue {}

components/TodoList : importとクラス定義

axiosを利用するのでインストールします。

% npm i axios
import { Component, Vue } from 'vue-property-decorator'
import axios from 'axios'
const NOT_STARTED = 1
const FINISHED = 3
@Component
export default class TodoList extends Vue {
   ここに実装
}

インスタンス変数定義

private todoList: string[] = []
private inputField: string = ''
private baseUrl: string = 'http://gin.taisablog.com/api/v1/'

createdフック

createdフックでロード時に一覧を取得します。

public created() {
  this.getTodo()
}

一覧を取得する

public async getTodo() {
  try {
    const response = await axios.get(this.baseUrl + 'todo')
    this.todoList = response.data
    return this.todoList
  } catch (e) {
    return e
  }
}

タスクを追加する

public async addTodo() {
  if (!this.inputField) {
    return
  }
  try {
    const params = {
      text: this.inputField,
      status: 1,
    }
    await axios.post(this.baseUrl + 'todo', JSON.stringify(params))
    this.getTodo()
    this.inputField = ''
  } catch (e) {
    return e
  }
}

タスクを削除する

public async deleteTodo(todo: any) {
  try {
    await axios.delete(this.baseUrl + 'todo/' + todo.ID)
    this.getTodo()
  } catch (e) {
    return e
  }
}

タスクを完了にする

public async toggle(todo: any) {
  try {
    let status = 0
    if (todo.Status === NOT_STARTED) {
      status = FINISHED
    } else {
      status = NOT_STARTED
    }
    const params = {
      '{status}': status,
    }
    await axios.put(this.baseUrl + 'todo/' + todo.ID, JSON.stringify(params))
    todo.Status = status
  } catch (e) {
    return e
  }
}

HTML

HTMLとCSSはもう少し書き直したいです。

<div class="todoList">
<h1>Vue.js TODO List</h1>
<section>
<div class="inputWrapper clearfix">
<div class="txtBoxWrapper">&nbsp;</div>
<div class="addBtnWrapper"><button class="addBtn">Add</button></div>
</div>
</section>
<section>
<div>
<ul>
<li>
<div class="todo"><label class="chkboxLabel"> </label>
<h5 class="todoTxt Finished">{{ todo.Text }}</h5>
<h5 class="todoTxt NotStarted">{{ todo.Text }}</h5>
<span class="deleteBtn">X</span></div>
</li>
</ul>
</div>
</section>
</div>











TodoList.vue全部

<div class="todoList">
<h1>Vue.js TODO List</h1>
<section>
<div class="inputWrapper clearfix">
<div class="txtBoxWrapper"> </div>
<div class="addBtnWrapper"><button class="addBtn">Add</button></div>
</div>
</section>
<section>
<div>
<ul>
<li>
<div class="todo"><label class="chkboxLabel"> </label>
<h5 class="todoTxt Finished">{{ todo.Text }}</h5>
<h5 class="todoTxt NotStarted">{{ todo.Text }}</h5>
<span class="deleteBtn">X</span></div>
</li>
</ul>
</div>
</section>
</div>
  import { Component, Vue } from 'vue-property-decorator'
  import axios from 'axios'
  const NOT_STARTED = 1
  const FINISHED = 3
  @Component
  export default class TodoList extends Vue {
    private todoList: string[] = []
    private inputField: string = ''
    private baseUrl: string = 'http://gin.taisablog.com/api/v1/'
    public created() {
      this.getTodo()
    }
    public async getTodo() {
      try {
        const response = await axios.get(this.baseUrl + 'todo')
        this.todoList = response.data
        return this.todoList
      } catch (e) {
        return e
      }
    }
    public async addTodo() {
      if (!this.inputField) {
        return
      }
      try {
        const params = {
          text: this.inputField,
          status: 1,
        }
        await axios.post(this.baseUrl + 'todo', JSON.stringify(params))
        this.getTodo()
        this.inputField = ''
      } catch (e) {
        return e
      }
    }
    public async deleteTodo(todo: any) {
      try {
        await axios.delete(this.baseUrl + 'todo/' + todo.ID)
        this.getTodo()
      } catch (e) {
        return e
      }
    }
    public async toggle(todo: any) {
      try {
        let status = 0
        if (todo.Status === NOT_STARTED) {
          status = FINISHED
        } else {
          status = NOT_STARTED
        }
        const params = {
          '{status}': status,
        }
        await axios.put(this.baseUrl + 'todo/' + todo.ID, JSON.stringify(params))
        todo.Status = status
      } catch (e) {
        return e
      }
    }
  }


  .todoList {
    width: 100%;
  }
  .clearfix::after {
    content: '';
    display: block;
    clear: both;
  }
  .inputWrapper {
      position: relative;
      width: 380px;
      margin: auto;
      display: block;
  }
  .inputWrapper input[type='text'] {
      font: 15px/24px sans-serif;
      box-sizing: border-box;
      width: 100%;
      padding: 0.3em;
      transition: 0.3s;
      letter-spacing: 1px;
      border: 1px solid #1b2538;
      border-radius: 4px;
  }
  .ef input[type='text']:focus {
      border: 1px solid #da3c41;
      outline: none;
      box-shadow: 0 0 5px 1px rgba(218, 60, 65, .5);
  }
  .txtBoxWrapper {
      float: left;
      width: 270px;
  }
  .addBtnWrapper {
      float: right;
  }
  .addBtn {
      position: relative;
      display: block;
      text-decoration: none;
      color: #FFF;
      background: #007bff;
      border: solid 1px #007bff;
      border-radius: 4px;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
      text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
      width: 100px;
      height: 35px;
      font-size: 16px;
  }
  ul {
      list-style: none;
  }
  li {
      border: 1px solid #dee2e6;
      border-top-left-radius: .25rem;
      border-top-right-radius: .25rem;
      margin: 10px auto;
      width: 50%;
      height: 80px;
  }
  .todo {
      display: flex;
      justify-content: space-between;
      align-items: center;
      width: 100%;
      height: 100%;
  }
  .chkboxLabel {
      width: 20px;
      display: inline-block;
      text-align: left;
      margin-right: 10px;
  }
  .chkbox {
      transform: scale(1.3);
      margin-left: 10px;
  }
  .todoTxt {
      font-size: 20px;
      width: 100%;
      text-align: center;
      vertical-align: middle;
      display: inline-block;
  }
  .todoTxt.NotStarted {
      text-decoration: none;
  }
  .todoTxt.Finished {
      text-decoration: line-through;
  }
  .deleteBtn {
      color: pink;
      text-align: right;
      margin-right: 20px;
      margin-left: 10px;
      width: 20px;
      display: block;
      font-size: 20px;
      cursor: pointer;
  }
  @media screen and (max-width: 520px) {
    ul {
       list-style: none;
       padding: 0;
       margin: 0;
    }
    li {
       border: 1px solid #dee2e6;
       border-top-left-radius: .25rem;
       border-top-right-radius: .25rem;
       margin: 10px 0;
       width: 100%;
       height: 80px;
       padding: 0;
    }
    .inputWrapper {
       margin: 0px auto
    }
  }

ソース

https://github.com/taisa831/sandbox-vuejs-ts

まとめ

TypeScriptを書くのが初めてだったので(書籍自体あまりなかったですが)事前に2冊購入して読みました。学習コストが大変かと思っていましたが、評判通りJavaC#のようなサーバサイドのように書けるので書きやすくてよいです。
[amazonjs asin="B0733113NK" locale="JP" title="速習TypeScript: altJSのデファクトスタンダートを素早く学ぶ! 速習シリーズ"]
[amazonjs asin="B00KMHSJ6I" locale="JP" title="JavaScriptプログラマのための 実践的TypeScript入門 (アスキー書籍)"]