はじめに
以前、Qiitaに書いた記事ですが、いくつか現行バージョンではうまくいかないという指摘があったため、やり直してみました。
各バージョンは、2018年3月24日時点の安定版で試しています。
作るもの
簡単なTODOアプリです。
TODOの管理はRailsのAPIで実施します。
前提条件、及び環境
Ruby、Rails、Node.jsの環境をご用意ください。
下記の記事などが分かりやすいです。
また、Webpack、およびVue.jsは
yarn
でインストールされるため、
yarn
もインストールしておいてください。
yarnを使ってみた
本記事を書くにあたり、下記の環境で実施しました。
- Ruby 2.5.0
- Ruby on Rails 5.1.5
- Node.js 9.9.0
- yarn 1.5.1
- rails/webpacker 3.3.1
- Vue.js 2.5.16
Webpack について
複数のJSファイルをひとまとめにするモジュールバンドラーのことです。
複数ファイルに分割して管理しつつ、サーバーへのリクエスト数を削減することができるようです。
最新版で学ぶwebpack 3入門 – JavaScript開発で人気のバンドルツール
準備
まずはもろもろインストールをしていきます。
なお、こちらの記事が動画つきで非常に分かりやすいので、もし初めて実施される方はご覧になった方が良いかと思います。
【動画付き】Rails 5.1で作るVue.jsアプリケーション ~Herokuデプロイからシステムテストまで~
Rails + Vue.jsのプロジェクトを作成する
はじめから
--webpack
オプションを使用してプロジェクトを作成します。
1 | $ 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
1 2 3 4 | class HomeController < ApplicationController def index end end |
1 2 3 | Rails.application.routes.draw do root to: 'home#index' end |
1 | <%= 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
1 | + gem 'foreman' |
これで
bundle install
します。
次に下記2点のファイルを作成します。
ファイル名 | 役割 |
---|---|
bin/server | Procfile.devのコマンドを実行する |
Procfile.dev |
rails s
と
bin/webpack-dev-server
を実行する |
1 2 3 | #!/bin/bash -i bundle install bundle exec foreman start -f Procfile.dev |
1 2 3 | web: bundle exec rails s # watcher: ./bin/webpack-watcher webpacker: ./bin/webpack-dev-server |
また、
bin/server
のパーミッションを変更しておきます。
1 | $ chmod 777 bin/server |
これで
bin/server
を実行してみると、
http://localhost:5000
で起動します。
(ポート番号が変わります。)
ここで、
app.vue
の下記の部分を変えて保存すると、コンパイル処理が走り、画面がリロードされるはずです。
適当に変えて遊んでみて、問題なさそうなら大丈夫です。
1 2 3 4 5 6 7 8 | export default { data: function () { return { // この文字列が画面に表示されている message: "Hello Vue!" } } } |
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
で作成してしまいます。
1 | $ rails generate model Task name:string is_done:boolean |
マイグレーションファイルに
null: false
と
default: false
を追記するため、下記のように編集します。
1 2 3 4 5 6 7 8 9 10 | 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 |
マイグレーションを実行します。
1 | $ rails db:migrate |
ついでに、モデルの
name
プロパティにバリデーションをつけておきましょう。
1 2 3 | class Task < ApplicationRecord + validates :name, presence: true end |
コントローラーと返却するJSONファイルを生成
アクセスするURLは
/api/tasks
のように名前空間を切りたいと思います。
まずはルーティングからです。
1 2 3 4 5 6 7 | 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
というディレクトリを作成し、そこに作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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
以下に作成します。
1 2 3 4 5 | json.set! :tasks do json.array! @tasks do |task| json.extract! task, :id, :name, :is_done, :created_at, :updated_at end end |
1 2 3 | json.set! :task do json.extract! @task, :id, :name, :is_done, :created_at, :updated_at end |
seeds.rbを作成し、curlコマンドで確認
seeds.rb
を作成し初期データを作成できるようにします。
もしレコードが増えすぎてもこれでやり直しもできます。
1 2 | 3.times { Task.create!(name: 'Sample Task') } 2.times { Task.create!(name: 'Sample Task', is_done: true) } |
DBに適用します。
1 | $ rails db:seed |
なお、リセットする場合は、
1 | $ rails db:setup |
curl
コマンドを使用してAPIの確認をします。
1 2 | $ 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で新規作成してみます。
ここで恐らくエラーになるかと思います。
1 | $ curl -X POST localhost:5000/api/tasks -d 'task[name]=fugafuga' |
log/development.log
を確認すると、
ActionController::InvalidAuthenticityToken
というエラーのようです。
CSRF対策のトークンがないため、Railsから怒られてしまいます。
application_controller.rb
の下記をコメントアウトするとエラーが出なくなります。
本来であれば、API認証のようなものをつけた方が良いとは思いますが、今回は割愛します。
【Rails】RailsでAPIの簡単なトークン認証を実装する
1 2 | - protect_from_forgery with: :exception + # protect_from_forgery with: :exception |
これで作成されたTODOが返却されるかと思います。
1 2 | $ 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"}} |
Materializeの導入
CSSはMaterializeというフレームワークを使用しようと思います。
マテリアルデザインを意識したものになっており、個人的に使ってみたいと思っておりました。
Gemもすでにあるので、Rails側でインストールします。
https://github.com/mkhairi/materialize-sass
1 2 3 | + gem 'jquery-rails' + gem 'materialize-sass' + gem 'material_icons' |
bundle install
した後、下記の2ファイルに追記します。
css
は
scss
に変更しておきます。
1 2 3 4 5 6 | + /* Materialize */ + @import "materialize/components/color"; + $primary-color: color("teal", "accent-3") !default; + $secondary-color: color("cyan", "base") !default; + @import 'materialize'; + @import 'material_icons'; |
$primary-color
と
$secondary-color
を設定しておくことで、よしなに色を合わせてくれます。
カラースキームはこちらを参照してください。
1 2 3 4 5 | + //= require jquery + //= require materialize //= require rails-ujs //= require turbolinks //= require_tree . |
コンポーネントを使ってヘッダーを作成
ここからVue.jsを中心に画面を作っていきます。
まずはヘッダーを作成します。
元となるビューファイルはRailsの
index.html.erb
になるので、こちらにVue.jsを載せられるようにします。
1 2 3 4 5 | <div id="app"> <navbar></navbar> </div> <%= javascript_pack_tag 'todo' %> |
<navbar>
というタグがありますが、Vue.js側でこのタグとコンポーネントを紐付け、表示します。
コンポーネント
また、新しく
todo.js
というファイルを
app/javascript/packs
に作成します。
(
hello_vue
を修正したも良いのですが。。。)
1 2 3 4 5 | 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
で作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <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
に登録します。
1 2 3 4 5 6 7 8 9 | 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
を追加します。
1 | $ yarn add vue-router |
今回は「TODO一覧(メイン画面)」、「アバウト(おまけ)」、「コンタクト(おまけ)」を用意します。
正直、メイン画面でTODO管理はできるので、あと2つはおまけです。
まずはコンポーネントを作成します。
1 2 3 4 5 | <template> <div> <p>Index</p> </div> </template> |
1 2 3 4 5 6 7 | <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> |
1 2 3 4 5 6 7 | <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
ディレクトリを作成してそちらに作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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>
の部分に表示します。
ヘッダーの各リンクを修正します。
1 2 3 4 5 6 | - <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
に作ります。
1 2 3 4 5 6 7 8 | <div id="app"> <navbar></navbar> + <div class="container"> + <router-view></router-view> + </div> </div> <%= javascript_pack_tag 'todo' %> |
最後に、
todo.js
に追加します。
1 2 3 4 5 6 7 8 9 10 11 | 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
にとぶよう記述します。
1 2 3 4 | 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通信ライブラリです。
まずはこれをインストールします。
1 | $ yarn add axios |
Axiosを使用して
index.vue
にてAPI通信してタスク管理したいと思います。
まずは完成イメージを
index.vue
の
template
内に記載します。
これを書き換えていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <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で取得するために必要なものを追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <!-- 省略 --> </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
名を作っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <!-- リスト表示部分 --> <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 の設計覚書
Webpack 3から、下記のように
loaders/vue.js
の中で
extractCSS = false
と修正すれば良いようです。
1 | + environment.loaders.get('vue').options.extractCSS = false |
index.vue
にスタイルを追記します。
<style>
タグ中に
scoped
という属性をつけておくと、そのファイルのみで有効なスタイルとして認識してくれます。
そのコンポーネントでしか使わないようなクラスはここで定義してしまえば良さそうです。
また、APIの返り値を使用してレンダリングする場合、API処理が終わるまでは、
{{ task.name }}
がそのまま文字列としてレンダリングされてしまいます。
(インスタンスに値がセットされたら、その値が表示されます。)
このとき、v-cloakというディレクティブをCSSの
display: none;
と組み合わせて使うと、インスタンスが生成されたタイミングで表示してくれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 省略 </script> + <style scoped> + [v-cloak] { + display: none; + } + .display_none { + display:none; + } + // 打ち消し線を引く + .line-through { + text-decoration: line-through; + } + </style> |
これで完了済みの方は非表示になったかと思います。
ボタンを押すと、完了済みエリアが表示される
まずはVueの
methods
にボタンを押されたときのメソッドを登録します。
1 2 3 4 5 6 7 8 | methods: { // 省略 }, + displayFinishedTasks: function() { + document.querySelector('#finished-tasks').classList.toggle('display_none'); + }, } } |
「ボタンを押されたとき」と記載するのはかなり簡潔にかけます。
テンプレートの方でv-onを使用してクリックされたタイミングで
displayFinishedTasks
を呼んでもらいます。
1 2 3 | <!-- 完了済みタスク表示ボタン --> - <div class="btn">Display finished tasks</div> + <div class="btn" v-on:click="displayFinishedTasks">Display finished tasks</div> |
新規作成フォームをつくる
v-modelを利用することで、双方向バインディングさせることができます。
これで
<input>
タグで入力された値とインスタンスの
newTask
プロパティをバインドさせます。
1 2 | - <input class="form-control" placeholder="Add your task!!"> + <input v-model="newTask" class="form-control" placeholder="Add your task!!"> |
次に新規作成のメソッドを追加します。
APIで新規作成できた場合は、
tasks
プロパティの先頭に追加するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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); + }); + } } |
チェックをつけたら完了済みに移す
まずはチェックボックスにチェックがついたら更新メソッドを呼ぶようにしたいと思います。
(この辺りは毎回呼ばずに、一定時間で同期させても良いかもしれません。)
1 2 3 4 5 6 7 8 9 10 | <!-- リスト表示部分 --> <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> |
更新用のメソッドを追加します。
更新したタイミングで、未完了部分から消し、完了済みの方に追加します。
(このあたりの処理はもっと良い書き方がある気がします。。。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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); + } } |
ここまでで、下図のような動きができるかと思います。
(いくつかスタイルはたしました。)
テストコードを書いてみる
試しにテストコードを書いてみました。
RailsのSystemTestCaseで書いております。
理由は、Node.jsのテストフレームワークは(私にとって)学習コストが高いためです。
本当はフロントエンドと分けるのであれば、テストも分けた方が良いのではないかと思っています。
https://github.com/naoki85/todo_app_with_vue_and_rails/commit/4a5225395fa2fe6ba9c7a3f8a421648ab28c897c
テストの中で、
within
を使用していますが、今回の場合は使用する必要はないと思います。
今後、さらに詳細なテストをする際に使用するかもと思い、残したままにしてあります。
まだ記事にできるほど試していないため、参考程度にご覧ください。
さいごに
今回は、状態管理に便利なVuexは今回できなかったため、機会があればまた書きたいと思います。
もしよろしければご意見いただけると嬉しいです。
参考にさせていただいたドキュメント、記事などは下記になります。
extractCSS = falseの設定ですが、こちらの記述をloader/vue.jsにするのが正しいです。
//コメントアウト
// const extractCSS = !(inDevServer && (devServer && devServer.hmr)) || isProduction
//これを追記
const extractCSS = false
匿名様
補足していただきありがとうございます。
とても参考になりました。ありがとうございます。
補足ですが、index.vue の newTask ボタンに v-on:click=”createTask” の記載が漏れているようです。ご確認ください。