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#のようなサーバサイドのように書けるので書きやすくてよいです。



関連記事