はじめに
業務で使っているわけではないのですが、個人的にコツコツVue.jsの勉強をしています。
今回は今まで勉強したことを整理する意味合いも兼ねて、チュートリアルのようなものを作成したいと思います。
見ていただいた方の参考になれば嬉しいです。
また、JavaScriptに関しては書き慣れていないので、もしもっと良い書き方などありましたらご意見いただけると幸いです。
なお、テストコードはまだ書けておりません。。。
次回の記事で各コンポーネントのユニットテストを書きたいと思っています。
2017/09/20 追記
試しに書いたテストコードの差分を、記事の最後に追記しました。
作るもの
簡単なTODOアプリです。
(どっかのサービスに似ていると言わないでください。。。)
TODOの管理はRailsのAPIで実施します。
前提条件及び環境
Ruby、Rails、Node.jsの環境をご用意ください。
下記の記事などが分かりやすいです。
また、Webpack、およびVue.jsはyarn
でインストールされるため、yarn
もインストールしておいてください。
yarnを使ってみた
本記事を書くにあたり、下記の環境で実施しました。
- Ruby 2.4.1
- Ruby on Rails 5.1.4
- Node.js 8.5.0
- yarn 1.0.2
- Webpacker 3.0.2
- Vue.js 2.4.3
Webpackについて
複数のJSファイルをひとまとめにするモジュールバンドラーのことです。
複数ファイルに分割して管理しつつ、サーバーへのリクエスト数を削減することができるようです。
最新版で学ぶwebpack 3入門 – JavaScript開発で人気のバンドルツール
準備
まずはもろもろインストールをしていきます。
なお、こちらの記事が動画つきで非常に分かりやすいので、もし初めて実施される方はご覧になった方が良いかと思います。
【動画付き】Rails 5.1で作るVue.jsアプリケーション ~Herokuデプロイからシステムテストまで~
Rails + Vue.jsのプロジェクトを作成する
はじめから--webpack
オプションを使用してプロジェクトを作成します。
$ rails new todo_sample --webpack=vue
rails s
でRailsのウェルカムページが表示されれば大丈夫です。
Vue.jsの表示確認
基本的に、Railsで用意するビューファイルは1つのみで、そこを差し替えていきます。
まずは、以下のファイルを作成、編集します。
app/controllers/home_controller.rb
config/routes.rb
app/views/home/index.html.erb
class HomeController < ApplicationController
def index
end
end
Rails.application.routes.draw do
root to: 'home#index'
end
<%= javascript_pack_tag 'hello_vue' %>
javascript_pack_tag
を使用することで、app/javascript/packs
以下にあるJSファイルを探してくれます。
インストール時にhello_vue.js
というファイルが生成されているので、これをindex
にて読み込ませます。
これでrails s
して、「Hello Vue!」と表示されれば大丈夫です。
devサーバーを設定する
Vue.js(というよりWebpack)関連のファイルは変更したらコンパイルする必要があります。
コンパイルには、bin/webpack
というコマンドを使用する必要があります。
ただ、毎回コンパイルするのは面倒なので、変更を検出して自動コンパイルするようにします。
こちらの記事でも紹介されております。
Introducing Webpacker
まずはforeman
のGemをインストールします。
foreman
はProcfile
から複数のプロセスを管理することができます。
https://github.com/ddollar/foreman
+ gem 'foreman'
これでbundle install
します。
次に下記2点のファイルを作成します。
ファイル名 | 役割 |
---|---|
bin/server | Procfile.devのコマンドを実行する |
Procfile.dev |
rails s とbin/webpack-dev-server を実行する |
#!/bin/bash -i
bundle install
bundle exec foreman start -f Procfile.dev
web: bundle exec rails s
# watcher: ./bin/webpack-watcher
webpacker: ./bin/webpack-dev-server
また、bin/server
のパーミッションを変更しておきます。
$ chmod 777 bin/server
これでbin/server
を実行してみると、http://localhost:5000
で起動します。
(ポート番号が変わります。)
ここで、app.vue
の下記の部分を変えて保存すると、コンパイル処理が走り、画面がリロードされるはずです。
適当に変えて遊んでみて、問題なさそうなら大丈夫です。
<script>
export default {
data: function () {
return {
// この文字列が画面に表示されている
message: "Hello Vue!"
}
}
}
</script>
APIの準備
サーバーサイドのAPI部分を実装していきます。
テーブルとモデルの生成
テーブル名はtasks
とし、下記のようにします。
カラム | 型 | |
---|---|---|
name | VARCHAR(255) | NULL: false |
is_done | BOOLEAN | NULL: false, DEFAULT: false |
created_at | DATETIME | NULL: false |
updated_at | DATETIME | NULL: false |
マイグレーションファイルとモデルはrails generate
で作成してしまいます。
$ rails generate model Task name:string is_done:boolean
マイグレーションファイルにnull: false
とdefault: false
を追記するため、下記のように編集します。
class CreateTasks < ActiveRecord::Migration[5.1]
def change
create_table :tasks do |t|
t.string :name, null: false
t.boolean :is_done, default: false, null: false
t.timestamps
end
end
end
マイグレーションを実行します。
$ rails db:migrate
ついでに、モデルのname
プロパティにバリデーションをつけておきましょう。
class Task < ApplicationRecord
+ validates :name, presence: true
end
コントローラーと返却するJSONファイルを生成
アクセスするURLは/api/tasks
のように名前空間を切りたいと思います。
まずはルーティングからです。
Rails.application.routes.draw do
root to: 'home#index'
+ namespace :api, format: 'json' do
+ resources :tasks, only: [:index, :create, :update]
+ end
end
次にコントローラーを作成します。
コントローラーはapp/controllers
の中にapi
というディレクトリを作成し、そこに作ります。
class Api::TasksController < ApplicationController
# GET /tasks
def index
# 後々のため、更新順で返します
@tasks = Task.order('updated_at DESC')
end
# POST /tasks
def create
@task = Task.new(task_params)
if @task.save
render :show, status: :created
else
render json: @task.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /tasks/1
def update
@task = Task.find(params[:id])
if @task.update(task_params)
render :show, status: :ok
else
render json: @task.errors, status: :unprocessable_entity
end
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def task_params
params.fetch(:task, {}).permit(
:name, :is_done
)
end
end
ビューのJSONファイルも同様に、views/api/tasks
以下に作成します。
json.set! :tasks do
json.array! @tasks do |task|
json.extract! task, :id, :name, :is_done, :created_at, :updated_at
end
end
json.set! :task do
json.extract! @task, :id, :name, :is_done, :created_at, :updated_at
end
seeds.rbを作成し、curlコマンドで確認
seeds.rb
を作成し初期データを作成できるようにします。
もしレコードが増えすぎてもこれでやり直しもできます。
3.times { Task.create!(name: 'Sample Task') }
2.times { Task.create!(name: 'Sample Task', is_done: true) }
DBに適用します。
$ rails db:seed
なお、リセットする場合は、
$ rails db:setup
curl
コマンドを使用してAPIの確認をします。
$ curl localhost:5000/api/tasks
{"tasks":[{"id":1,"name":"Sample Task","is_done":false,"created_at":"2017-09-14T08:12:35.454Z","updated_at":"2017-09-14T08:12:35.454Z"},{"id":2,"name":"Sample Task","is_done":false,"created_at":"2017-09-14T08:12:35.460Z","updated_at":"2017-09-14T08:12:35.460Z"},{"id":3,"name":"Sample Task","is_done":false,"created_at":"2017-09-14T08:12:35.462Z","updated_at":"2017-09-14T08:12:35.462Z"},{"id":4,"name":"Sample Task","is_done":true,"created_at":"2017-09-14T08:12:35.468Z","updated_at":"2017-09-14T08:12:35.468Z"},{"id":5,"name":"Sample Task","is_done":true,"created_at":"2017-09-14T08:12:35.475Z","updated_at":"2017-09-14T08:12:35.475Z"}]}
次にPOSTで新規作成してみます。
ここで恐らくエラーになるかと思います。
$ curl -X POST localhost:5000/api/tasks -d 'task[name]=fugafuga'
エラー内容はCan't verify CSRF token authenticity.Completed 422 Unprocessable Entity ~~
です。
CSRF対策のトークンがないため、Railsから怒られてしまいます。
application_controller.rb
の下記をコメントアウトするとエラーが出なくなります。
本来であれば、API認証のようなものをつけた方が良いとは思いますが、今回は割愛します。
【Rails】RailsでAPIの簡単なトークン認証を実装する
- protect_from_forgery with: :exception
+ # protect_from_forgery with: :exception
これで作成されたTODOが返却されるかと思います。
$ curl -X POST localhost:5000/api/tasks -d 'task[name]=fugafuga'
{"task":{"id":6,"name":"fugafuga","is_done":false,"created_at":"2017-09-14T08:31:17.100Z","updated_at":"2017-09-14T08:31:17.100Z"}}
2020/10/18 追記
@ohara5555 さんにご指摘いただきました。
Rails 5.2 以降のバージョンを使用されている場合は、 application_controller に protect_from_forgery がないので、(ApplicationController::Base に移動した)
自分でこのように記載していただくか、
class Api::TasksController < ApplicationController
+ protect_from_forgery
end
config で設定値を上書きもできるようです。
https://edgeguides.rubyonrails.org/configuring.html#configuring-action-controller
Materializeの導入
CSSはMaterializeというフレームワークを使用しようと思います。
マテリアルデザインを意識したものになっており、個人的に使ってみたいと思っておりました。
Gemもすでにあるので、Rails側でインストールします。
https://github.com/mkhairi/materialize-sass
ドキュメントにもありますが、Turbolinksを使用する場合は、jquery-turbolinksを入れた方が良いそうです。
今回は画面遷移としてはないので、Turbolinksが影響することはなさそうですが、とりあえず入れておきます。
2018/11/9 追記
jquery-turbolinks は必要なさそうなので削除しました。
+ gem 'jquery-rails'
+ gem 'materialize-sass'
+ gem 'material_icons'
bundle install
した後、下記の2ファイルに追記します。
css
はscss
に変更しておきます。
2018/11/9 追記
@ryo117n さんよりご指摘があり、 color
の import
文を修正しました。
+ /* Materialize */
+ @import "materialize/components/color-variables";
+ $primary-color: color("teal", "accent-3") !default;
+ $secondary-color: color("cyan", "base") !default;
+ @import 'materialize';
+ @import 'material_icons';
$primary-color
と$secondary-color
を設定しておくことで、よしなに色を合わせてくれます。
カラースキームはこちらを参照してください。
+ //= require jquery
+ //= require materialize
//= require rails-ujs
//= require turbolinks
//= require_tree .
コンポーネントを使ってヘッダーを作成
ここからVue.jsを中心に画面を作っていきます。
まずはヘッダーを作成します。
元となるビューファイルはRailsのindex.html.erb
になるので、こちらにVue.jsを載せられるようにします。
<div id="app">
<navbar></navbar>
</div>
<%= javascript_pack_tag 'todo' %>
<navbar>
というタグがありますが、Vue.js側でこのタグとコンポーネントを紐付け、表示します。
コンポーネント
また、新しくtodo.js
というファイルをapp/javascript/packs
に作成します。
(hello_vue
を修正したも良いのですが。。。)
import Vue from 'vue/dist/vue.esm.js'
var app = new Vue({
el: '#app',
});
ここで、import Vue from 'vue/dist/vue.esm.js'
としています。
これは、後ほどコンポーネントを使用する際に完全ビルドする必要があるからだそうです。
(すいません、まだよく分かっていません。。。)
詳しくはこちらの方が解説記事を書いてくださっています。
Rails5.1でVue.jsで単一ファイルコンポーネントのエラーがでる
これでindex.html.erb
内の<div id="app">
にマウントされます。
このまま実行しても特に何もありません。
それではコンポーネントを作成します。
packs
の下にcomponents
ディレクトリを作成して、そこにheader.vue
を作成します。
コンポーネントは.vue
で作成します。
<template>
<div>
<ul id="dropdown" class="dropdown-content">
<li><a href="#">Top</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<nav>
<div class="nav-wrapper container">
<a href="/" class="brand-logo left">Todo Application</a>
<ul class="right hide-on-med-and-down">
<li><a href="#">Top</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<ul class="right hide-on-large-only">
<li>
<a class="dropdown-button" href="#!" data-activates="dropdown">
Menu<i class="material-icons right">arrow_drop_down</i>
</a>
</li>
</ul>
</div>
</nav>
</div>
</template>
これをtodo.js
に登録します。
import Vue from 'vue/dist/vue.esm.js'
+ import Header from './components/header.vue'
var app = new Vue({
el: '#app',
+ components: {
+ 'navbar': Header,
+ }
});
navbar
という名前でコンポーネントとして登録します。
(header
だと<header>
タグがすでにHTML5に存在しているため。)
これで<navbar>
タグが使用できるようになりました。
サーバーを再起動してアクセスすると下図のようなヘッダーができているのではないでしょうか?
Vue-Routerを使用してSPAっぽく
Vue-Routerを使用することで、登録されたパスとコンポーネントで画面内を差し替えることができます。
Vue-Router
yarn
を使ってvue-router
を追加します。
$ yarn add vue-router
今回は「TODO一覧(メイン画面)」、「アバウト(おまけ)」、「コンタクト(おまけ)」を用意します。
正直、メイン画面でTODO管理はできるので、あと2つはおまけです。
まずはコンポーネントを作成します。
<template>
<div>
<p>Index</p>
</div>
</template>
<template>
<div>
<!-- 内容はお好みで -->
<p>This is a sample of TODO application with Vue.js and Ruby on Rails.</p>
<p>Sample code is <a href="https://github.com/naoki85/todo_app_with_vue_and_rails" target="_blank">here.</a></p>
</div>
</template>
<template>
<div>
<!-- 内容はお好みで -->
<p>If you want to contact me, you send mail to below address.</p>
<p>test@example.com</p>
</div>
</template>
さて、このコンポーネントとパスを登録するrouter.js
を作成します。
こちらもrouter
ディレクトリを作成してそちらに作成します。
import Vue from 'vue/dist/vue.esm.js'
import VueRouter from 'vue-router'
import Index from '../components/index.vue'
import About from '../components/about.vue'
import Contact from '../components/contact.vue'
Vue.use(VueRouter)
export default new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Index },
{ path: '/about', component: About },
{ path: '/contact', component: Contact },
],
})
パスとコンポーネントを結びつけます。
また、mode: 'history'
とすることで、HTMLのhistory APIを使用して、一見同じビュー内ですがURLを書き換えることができます。
HTML5 Historyモード
また、VueRouterを使用すると、<router-link>
と<router-view>
というタグが使用できます。
<router-link>
は、<a>
タグとして変換されますが、画面遷移ではなくVueRouterに登録されたパスからコンポーネントを探します。
そして<router-view>
の部分に表示します。
ヘッダーの各リンクを修正します。
- <li><a href="/">Top</a></li>
- <li><a href="/about">About</a></li>
- <li><a href="/contact">Contact</a></li>
+ <li><router-link to="/">Top</router-link></li>
+ <li><router-link to="/about">About</router-link></li>
+ <li><router-link to="/contact">Contact</router-link></li>
それぞれのコンポーネントが表示される部分をindex.html.erb
に作ります。
<div id="app">
<navbar></navbar>
+ <div class="container">
+ <router-view></router-view>
+ </div>
</div>
<%= javascript_pack_tag 'todo' %>
最後に、todo.js
に追加します。
import Vue from 'vue/dist/vue.esm.js'
+ import Router from './router/router'
import Header from './components/header.vue'
var app = new Vue({
+ router: Router,
el: '#app',
components: {
'navbar': Header,
}
});
これで、ヘッダーの各リンクを押すと本文が切り替わるのではないでしょうか?
URLもHistoryモードのおかげで書き換わっています。
ただ、例えばhttp://localhost:5000/about
でリロードすると、Rails側でエラーになってしまいます。
たしかにroutes.rb
で登録していません。
とりあえず、/about
でも/contact
でもHome#index
にとぶよう記述します。
Rails.application.routes.draw do
root to: 'home#index'
+ get '/about', to: 'home#index'
+ get '/contact', to: 'home#index'
これでhttp://localhost:5000/about
にアクセスするとわかりますが、ちゃんとURLからAboutのコンポーネントを表示してくれます。
Axiosを使ってAPI通信
axiosは、Ajax通信ライブラリです。
まずはこれをインストールします。
$ yarn add axios
Axiosを使用してindex.vue
にてAPI通信してタスク管理したいと思います。
まずは完成イメージをindex.vue
のtemplate
内に記載します。
これを書き換えていきます。
2019/01/12 追記
Materialize の最新バージョンを使用する場合、本記事時点の文法とは異なる場合があります。
@tktcorporation さんより、チェックボックスの修正方法のコメントをいただきました。
もしスタイルやJSがうまく当たっていないと感じた場合は、 Materialize のドキュメントを参考にしていただきますようお願いします。
<template>
<div>
<!-- 新規作成部分 -->
<div class="row">
<div class="col s10 m11">
<input class="form-control" placeholder="Add your task!!">
</div>
<div class="col s2 m1">
<div class="btn-floating waves-effect waves-light red">
<i class="material-icons">add</i>
</div>
</div>
</div>
<!-- リスト表示部分 -->
<div>
<ul class="collection">
<li id="row_task_1" class="collection-item">
<input type="checkbox" id="task_1" />
<label for="task_1">Sample Task</label>
</li>
<li id="row_task_2" class="collection-item">
<input type="checkbox" id="task_2" />
<label for="task_2">Sample Task</label>
</li>
<li id="row_task_3" class="collection-item">
<input type="checkbox" id="task_3" />
<label for="task_3">Sample Task</label>
</li>
</ul>
</div>
<!-- 完了済みタスク表示ボタン -->
<div class="btn">Display finished tasks</div>
<!-- 完了済みタスク一覧 -->
<div id="finished-tasks" class="display_none">
<ul class="collection">
<li id="row_task_4" class="collection-item">
<input type="checkbox" id="'task_4" checked="checked" />
<label v-bind:for="task_4" class="line-through">Done Task</label>
</li>
<li id="row_task_5" class="collection-item">
<input type="checkbox" id="'task_5" checked="checked" />
<label v-bind:for="task_5" class="line-through">Done Task</label>
</li>
</ul>
</div>
</div>
</template>
こんな感じになります。
一覧表示
コンポーネントの中でHTML、JS、CSSをまとめて記載することを単一コンポーネントというようです。
.vue
ファイルの中で、そのコンポーネントで使うJSも記述することができます。
一覧をAPIで取得するために必要なものを追記します。
<!-- 省略 -->
</template>
+ <script>
+ import axios from 'axios';
+
+ export default {
+ data: function () {
+ return {
+ tasks: [],
+ newTask: ''
+ }
+ },
+ mounted: function () {
+ this.fetchTasks();
+ },
+ methods: {
+ fetchTasks: function () {
+ axios.get('/api/tasks').then((response) => {
+ for(var i = 0; i < response.data.tasks.length; i++) {
+ this.tasks.push(response.data.tasks[i]);
+ }
+ }, (error) => {
+ console.log(error);
+ });
+ },
+ }
+ }
+ </script>
インスタンスにプロパティとしてtasks
とnewTask
を与えます。
メソッドとしてfetchTasks
を登録し、APIで取得してきた値をループさせてtasks
に格納します。
AxiosはJQueryと同じ感じで使えるので、使用しやすいかと思います。
mountedはVueインスタンスがマウントされたタイミングで実行されるライフサイクルフックです。
createdもあって今回の場合、あまり違いはありませんが、ライフサイクルダイアグラムについては、ちゃんと理解する必要がありそうです。
template
内の下記の部分を書き換えます。
(未完了と完了済みを両方変えます。)
v-for
とv-if
を使ってtasks
プロパティの中で条件に合うものを表示しています。
条件付きレンダリング
また、v-bindで強引にid
名を作っています。
<!-- リスト表示部分 -->
<div>
<ul class="collection">
- <li id="row_task_1" class="collection-item">
- <input type="checkbox" id="task_1" />
- <label for="task_1">Sample Task</label>
- </li>
- <li id="row_task_2" class="collection-item">
- <input type="checkbox" id="task_2" />
- <label for="task_2">Sample Task</label>
- </li>
- <li id="row_task_3" class="collection-item">
- <input type="checkbox" id="task_3" />
- <label for="task_3">Sample Task</label>
- </li>
+ <li v-for="task in tasks" v-if="!task.is_done" v-bind:id="'row_task_' + task.id" class="collection-item">
+ <input type="checkbox" v-bind:id="'task_' + task.id" />
+ <label v-bind:for="'task_' + task.id">{{ task.name }}</label>
+ </li>
</ul>
</div>
<!-- 完了済みタスク一覧 -->
<div id="finished-tasks" class="display_none">
<ul class="collection">
- <li id="row_task_4" class="collection-item">
- <input type="checkbox" id="'task_4" checked="checked" />
- <label v-bind:for="task_4" class="line-through">Done Task</label>
- </li>
- <li id="row_task_5" class="collection-item">
- <input type="checkbox" id="'task_5" checked="checked" />
- <label v-bind:for="task_5" class="line-through">Done Task</label>
- </li>
+ <li v-for="task in tasks" v-if="task.is_done"v-bind:id="'row_task_' + task.id" class="collection-item">
+ <input type="checkbox" v-bind:id="'task_' + task.id" checked="checked" />
+ <label v-bind:for="'task_' + task.id" class="line-through">{{ task.name }}</label>
+ </li>
</ul>
</div>
完了済みタスクを常に表示させておく必要はないと思います。
display_none
というクラスをつけているので、ここに非表示のスタイルをあてたいと思います。
単一コンポーネントではCSSも管理できます。
ただ、デフォルトの設定だと、コンパイル時に別でスタイルシートを出力してしまうので、これを無しにします。
RailsとVue.js の設計覚書
スタイルシートをまとめて出力
+ environment.loaders.get('vue').options.extractCSS = false
2018/03/24 追記
@trandaison さんよりご指摘がありました。
webpacker 3+から、loaders/vue.js
の中でextractCSS = false
と修正すれば良いようです。
(devServerを見直せば良さそうですが。。)
//const extractCSS = !(inDevServer && (devServer && devServer.hmr)) || isProduction
const extractCSS = false
ただ、私が作成した時にこのファイルはなかったので、バージョンアップの際に追加されたのかもしれません。
(そこまでは追っていませんでした。。。)
loaders/vue.js
がないバージョンをお使いの方は @kawadumax さんからもご指摘がありましたが、下記のように extractCSS
を指定すれば良いようです。
+ environment.loaders.get('vue').use[0].options.extractCSS = false
スタイルの追加
index.vue
にスタイルを追記します。
<style>
タグ中にscoped
という属性をつけておくと、そのファイルのみで有効なスタイルとして認識してくれます。
そのコンポーネントでしか使わないようなクラスはここで定義してしまえば良さそうです。
また、APIの返り値を使用してレンダリングする場合、API処理が終わるまでは、{{ task.name }}
がそのまま文字列としてレンダリングされてしまいます。
(インスタンスに値がセットされたら、その値が表示されます。)
このとき、v-cloakというディレクティブをCSSのdisplay: none;
と組み合わせて使うと、インスタンスが生成されたタイミングで表示してくれます。
// 省略
</script>
+ <style scoped>
+ [v-cloak] {
+ display: none;
+ }
+ .display_none {
+ display:none;
+ }
+ // 打ち消し線を引く
+ .line-through {
+ text-decoration: line-through;
+ }
+ </style>
これで完了済みの方は非表示になったかと思います。
ボタンを押すと、完了済みエリアが表示される
まずはVueのmethods
にボタンを押されたときのメソッドを登録します。
methods: {
// 省略
},
+ displayFinishedTasks: function() {
+ document.querySelector('#finished-tasks').classList.toggle('display_none');
+ },
}
}
「ボタンを押されたとき」と記載するのはかなり簡潔にかけます。
テンプレートの方でv-onを使用してクリックされたタイミングでdisplayFinishedTasks
を呼んでもらいます。
<!-- 完了済みタスク表示ボタン -->
- <div class="btn">Display finished tasks</div>
+ <div class="btn" v-on:click="displayFinishedTasks">Display finished tasks</div>
新規作成フォームをつくる
v-modelを利用することで、双方向バインディングさせることができます。
これで<input>
タグで入力された値とインスタンスのnewTask
プロパティをバインドさせます。
- <input class="form-control" placeholder="Add your task!!">
+ <input v-model="newTask" class="form-control" placeholder="Add your task!!">
次に新規作成のメソッドを追加します。
APIで新規作成できた場合は、tasks
プロパティの先頭に追加するようにします。
methods: {
// 省略
},
+ createTask: function () {
+ if (!this.newTask) return;
+
+ axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => {
+ this.tasks.unshift(response.data.task);
+ this.newTask = '';
+ }, (error) => {
+ console.log(error);
+ });
+ }
}
作成ボタンを押した場合にこのメソッドを呼びたいので、v-onで登録します。
- <div class="btn-floating waves-effect waves-light red">
+ <div v-on:click="createTask" class="btn-floating waves-effect waves-light red">
<i class="material-icons">add</i>
</div>
チェックをつけたら完了済みに移す
まずはチェックボックスにチェックがついたら更新メソッドを呼ぶようにしたいと思います。
(この辺りは毎回呼ばずに、一定時間で同期させても良いかもしれません。)
<!-- リスト表示部分 -->
<div>
<ul class="collection">
<li v-bind:id="'row_task_' + task.id" class="collection-item" v-for="task in tasks" v-if="!task.is_done">
- <input type="checkbox" v-bind:id="'task_' + task.id" />
+ <input type="checkbox" v-on:change="doneTask(task.id)" v-bind:id="'task_' + task.id" />
<label v-bind:for="'task_' + task.id" class="word-color-black">{{ task.name }}</label>
</li>
</ul>
</div>
更新用のメソッドを追加します。
更新したタイミングで、未完了部分から消し、完了済みの方に追加します。
(このあたりの処理はもっと良い書き方がある気がします。。。)
methods: {
// 省略
},
+ doneTask: function (task_id) {
+ axios.put('/api/tasks/' + task_id, { task: { is_done: 1 } }).then((response) => {
+ this.moveFinishedTask(task_id);
+ }, (error) => {
+ console.log(error);
+ });
+ },
+ moveFinishedTask: function(task_id) {
+ var el = document.querySelector('#row_task_' + task_id);
+ // DOMをクローンしておく
+ var el_clone = el.cloneNode(true);
+ // 未完了の方を先に非表示にする
+ el.classList.add('display_none');
+ // もろもろスタイルなどをたして完了済みに追加
+ el_clone.getElementsByTagName('input')[0].checked = 'checked';
+ el_clone.getElementsByTagName('label')[0].classList.add('line-through');
+ el_clone.getElementsByTagName('label')[0].classList.remove('word-color-black');
+ var li = document.querySelector('#finished-tasks > ul > li:first-child');
+ document.querySelector('#finished-tasks > ul').insertBefore(el_clone, li);
+ }
}
さいごに
ここまでで、下図のような動きができるかと思います。
(いくつかスタイルはたしました。)
ただ、テスト、および状態管理に便利なVuexは今回できなかったため、機会があればまた書きたいと思います。
もしよろしければご意見いただけると嬉しいです。
最後に、今回のソースコードはこちらになります。
https://github.com/naoki85/todo_app_with_vue_and_rails
2017/09/20 追記
試しにテストコードを書いてみました。
RailsのSystemTestCaseで書いております。
理由は、Node.jsのテストフレームワークは(私にとって)学習コストが高いためです。
本当はフロントエンドと分けるのであれば、テストも分けた方が良いのではないかと思っています。
https://github.com/naoki85/todo_app_with_vue_and_rails/commit/4a5225395fa2fe6ba9c7a3f8a421648ab28c897c
テストの中で、within
を使用していますが、今回の場合は使用する必要はないと思います。
今後、さらに詳細なテストをする際に使用するかもと思い、残したままにしてあります。
まだ記事にできるほど試していないため、参考程度にご覧ください。
参考にさせていただいたドキュメント、記事などは下記になります。