Vue.jsでシンプルなTodoアプリの作成

Vue.jsの公式サイトにあるサンプルのTodoアプリをトレースしたので記事にまとめます。

制作物の概要

↓作ったのは下図のようなシンプルなTodoアプリです。

ブラウザを閉じたら情報は消えますが、DBなど用意して保存できるようにすればもっとアプリっぽくできますね。

使用技術

  • Vue.js:2.5.17
  • Semantic UI:2.4.1

ディレクトリ構成

todo_list/
├── index.html
├── script.js
└── style.css

index.html

Vue.jsはCDNのリンクを読み込んでいます。Semantic UIも同様です。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Vue.js ToDo List</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <nav class="navbar" role="navigation" aria-label="main navigation">
        <div class="container">
            <div class="navbar-brand">
                <h1 class="title">Vue.js ToDo List</h1>
            </div>
        </div>
    </nav>
    <div class="ui text container" id="app">
        <h2 class="ui dividing header">My To Do List</h2>
        <div class="ui segments form">
            <div class="ui segment field">
                <input placeholder="What needs to be done?"
                v-model="newToDo"
                v-on:keyup.enter="addToDo"
                v-on:keypress.enter="setCanSubmit()">
            </div>
            <div class="ui segments horizontal">
                <div class="ui segment">
                    <p>{{todoLength}} items left.</p>
                </div>
                <div class="ui segment"
                    style="text-transform: capitalize;"
                    v-for="(item, index) in filterTypes"
                    v-bind:key="index">
                    <a v-bind:href="`#/${item}`"
                        v-bind:class="{ selected: filter == item }"
                        v-on:click="filter = item">
                        {{item}}
                    </a>
                </div>
                <div class="ui segment">
                    <a href="#"
                        v-on:click="clearToDo()">
                        Clear completed
                    </a>
                </div>
            </div>
            <div class="ui segment" v-if="todos.length === 0">Nothing to do...</div>
            <div class="ui segment" v-else v-for="(todo, index) in filteredTodos" v-bind:key="index">
                <div class="ui checkbox">
                    <input
                        type="checkbox"
                        v-model="todo['done']">
                    <label>
                        <input type="text" class="edit"
                            v-bind:class="{donestyle:todo['done']}"
                            v-bind:disabled="todo['done']"
                            v-bind:value="todo['todo']"
                            v-on:blur="doneEdit(index, $event.target.value)"
                            v-on:keyup.enter="doneEdit(index, $event.target.value)"
                            v-on:keyup.esc="cancelEdit(index, todo['todo'])"
                            v-on:keypress.enter="setCanSubmit()">
                        <transition>
                            <span
                                class="badge ui green text"
                                v-if="todo['done']">
                                Done!!
                            </span>
                        </transition>
                    </label>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="script.js"></script>
</body>
</html>

使い回すパーツはコンポーネント化することも考えましたが、for文でのリファクタリングに留めました。

script.js

var filters = {
    'all': function (todos) {
        return todos
    },
    'active': function (todos) {
        return todos.filter(function (todo) {
            return !todo['done']
        })
    },
    'done': function (todos) {
        return todos.filter(function (todo) {
            return todo['done']
        })
    }
}
new Vue({
    el: '#app',
    data: {
        newToDo: '',
        todos: [],
        filter: 'all',
        filterTypes: ['all', 'active', 'done'],
        canSubmit: false
    },
    methods: {
        addToDo: function() {
            if(this.canSubmit) {
                this.todos.push({'done': false, 'todo': this.newToDo});
                this.newToDo = '';
                this.canSubmit = false;
            }
        },
        clearToDo: function() {
            this.todos = this.todos.filter(
                function(item) {
                    return item['done']===false;
                }
            )
        },
        doneEdit: function(index, editedToDo) {
            if(this.canSubmit){
                this.todos[index]['todo'] = editedToDo;
                this.canSubmit = false;
            }
        },
        cancelEdit: function(index, todo) {
            this.todos[index]['todo'] = '';
            this.todos[index]['todo'] = todo;
        },
        setCanSubmit: function() {
            this.canSubmit = true;
        }
    },
    computed: {
        filteredTodos: function() {
            return filters[this.filter](this.todos);
        },
        todoLength: function() {
            return filters['active'](this.todos).length;
        }
    }
})

style.css

.text.green {
    color: #32CD32;
}

.v-enter-active, .v-leave-active {
    transition: .3s ease;
}
.v-enter, .v-leave-to {
    opacity: 0;
    transform: translateX(10px);
}

nav {
    margin-bottom: 1rem;
}
a.selected {
    text-decoration: underline;
}
.ui.checkbox {
    width: 100%!;(MISSING)
}
.ui.checkbox label .donestyle,
.ui.checkbox+label .donestyle,
.ui.checkbox input:focus~label .donestyle {
    text-decoration: line-through;
    color: lightgray;
}
.badge {
    margin-left: 1rem;
    display: inline-block;
    position: absolute;
    top: 0;
    right: 0;
}
.ui.form input[type=text].edit {
    border: 1px solid #fff;
    margin-top: -9.5px;
    margin-bottom: -9.5px;
}
.ui.form input[type=text].edit:focus {
    border: 1px solid #cce2ff;
}

工夫した点

工夫したと言うか、詰まってしまったので考えざるおえなかった箇所について。

日本語変換時の処理

index.htmlのinput要素に v-on:keyup.enter を仕込んでいて、キーボードのEnter押下をトリガーにタスクを確定をしているのですが、日本語の変換時のEnterで処理が走ってしまいました。この問題を防ぐため、 setCanSubmit という関数でフラグを立てるようにしてから処理が走るようにしました。

部分的にv-modelではなくv-bindでつなぎこみ

inputタグとvue.jsのつなぎこみはv-modelを使うのがセオリーのようなのですが、タスクの編集機能でうまく実装できませんでした。最終的にはinputのvalue属性をv-bindでつなぎ、v-on:keyup.enterをトリガーに関数を実行してゴニョゴニョしています。

まとめ

Vue.jsの基本的な部分はキャッチアップできたかなと思います。実務ではVue CLIとWebpackは必須なので、次はそこらへんに取り組みます。