Edited at

[Rails4/5] Vue.jsでシングルページアプリケーションの構築を目指す(登録、一覧表示の実装編)

More than 1 year has passed since last update.


前提

普段はRuby on Railsを使いつつ、フロントエンド強化のため、Vue.jsを学習中。

Railsで実装したアプリケーションをVue.jsを使って作り変えるをお題に、シンプルなサンプルを作成します。

Vue.jsを使って極力ページ遷移しないアプリケーションを作っていきます。


やること


  • 簡易的なブログ機能を想定したアプリケーションをイメージしてRailsのScaffodコマンドでベースになる部分を作成。


  • 登録と一覧表示処理について画面遷移しない形になるようにVue.jsを利用してその機能を実現。


Vue.js初心者向けの内容になります。


サンプルアプリはこちら

vue-js-articles


下準備


Railsで土台を作成する

Scaffoldコマンドでブログ記事管理用(Article)のModel, View, Controllerを作成。

Modelにはタイトルと本文用のカラムを作成。

$ rails g scaffold article title body:text

データベース作成、migration実行

$ bin/rails db:create

$ bin/rails db:migrate

おなじみの記事のCRUDを行うアプリが完成。

スクリーンショット 2017-08-18 14.01.18.png

RailsでできたこのアプリをVue.jsを使う形に編集していく〜!


Vue.jsのインストール

Vue.js公式サイトからソースファイルをダウンロードして使用する。開発バージョンを選択。

app/assets/javascriptsに配置し、application.jsで読み込む。


app/assets/javascripts/application.js

//= require jquery

//= require jquery_ujs
//= require turbolinks
+ //= require vue
//= require_tree .

ブラウザでVue.jsが読み込まれていることを確認

Chromeの場合、右クリック 検証 > ConsoleタブでVue.jsが読み込まれているメッセージが表示されていればOK。

スクリーンショット_2017-08-18_15_06_05.png


記事の新規登録の実装

下記のステップで進める。


  • 新規作成フォームを一覧ページに表示する

  • 記事を登録する


1. 新規作成フォームを表示する

Railsのform_forを使う形ではなく、HTMLのタグを使ってフォームを描画。

Railsでは最低限のことを行い、Javascriptで実装する!


実装イメージ

New Article ボタンを押下することで、フォームの表示/非表示が切り替えれるようにする。

github.gif


一覧表示用のView, JavaScriptファイルを編集


app/views/articles/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Articles</h1>

<table>
<thead>
<tr>
<th>Title</th>
<th>Body</th>
<th colspan="3"></th>
</tr>
</thead>

<tbody>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.body %></td>
<td><%= link_to 'Show', article %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
<td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>

<br>

- <%= link_to 'New Article', new_article_path %>

+ <div id="article-form">
+ <button @click="show=!show" class="btn btn-info">New Article</button>
+ <div v-show="show">
+ <div>
+ <label for="title">Title</label>
+ <input type="text" />
+ </div>
+ <div>
+ <label for="body">Body</label>
+ <textarea></textarea>
+ </div>
+ <div>
+ <button class="btn btn-primary">Submit</button>
+ </div>
+ </div>
+ </div>


今回CoffeeScriptでなく通常のJavaScriptで記述したいので、.jsファイル作成。

$ touch app/assets/javascripts/articles.js


app/assets/javascripts/articles.js

window.onload = function() {

var vm = new Vue({
el: '#article-form',
data: {
show: false
}
});
};


実装ポイント


  • Vueインスタンス作成

    View側でbuttonとformの上位のdivにidセレクター(article-form)を設定し、JS側でVueインスタンスnew Vueを作成。



  • 新規登録用のフォーム設置

    View側で新規作成用のフォーム要素を設置。HTMLのタグ(今回はinput, textarea)を使って追加。



  • フォームにv-showディレクティブを設定

    View側のフォーム要素の上位divv-show="show"を設定。これにより、JS側で与えているdataオブジェクトのshowの値がtrueの場合のみdivを表示する。

    v-showについて



  • ボタンにイベントハンドラを設定

    Vuew側のNew Articleリンクを削除し、buttonタグを設置

    buttonタグにイベントハンドラとしてv-on:clickを設定(@clickは省略記法)

    @clickshowの値がtrue/falseと切り変わるように記述。

    JS側でshowのデフォルト値にfalseに設定しているので通常時は表示されない。

    v-on:clickについて



2. 記事を登録する


フォームの入力値をJS側で受け取る

View側のtitle, bodyの入力欄にv-modelを設定。v-modelについて。

Submitボタンにメソッドを実行するイベントハンドラ@clickを設定。


app/views/articles/index.html.erb

(中略)

<div>
<label for="title">Title</label>
<input type="text" v-model="title" />
</div>
<div>
<label for="body">Body</label>
<textarea v-model="body"></textarea>
</div>
<div>
<button @click="create" class="btn btn-primary">Submit</button>
</div>

JS側でフォームの入力値が受け取れるか取得してみる。

title, bodyをdataオブジェクトに追加。

methodsを定義し、メソッド内で受け取ったものをコンソールに表示してみる。


app/assets/javascripts/articles.js

window.onload = function() {

var vm = new Vue({
el: '#article-form',
data: {
show: false,
title: '',
body: ''
},
methods: {
create: function() {
console.log(this.title); //入力したタイトルが表示
console.log(this.body);  //入力した内容が表示
}
}
});
};


Ajaxを使ってRails側へ通信を行う。(データベースへの保存はまだ行わない)

jQueryのおなじみの呼び出し方を使ってAjaxを行い、記事の登録処理を行いたいので、ArticleのcreateアクションをPOSTで呼び出す。

また、フォームから入力された値をパラメータとして送る。

ブラウザで通信中/完了のステータスが分かるように下記の実装も併せて行う。


  • 通信中は「通信中です」というメッセージを表示させる

  • 通信中はSubmitボタンを押下できないようにする

これらはdataオブジェクトにプロパティmessage, loadingを追加し実装する。


app/assets/javascripts/articles.js

window.onload = function() {

var vm = new Vue({
el: '#article-form',
data: {
show: false,
loading: false,
message: '送信中です',
title: '',
body: ''
},
methods: {
create: function() {
this.loading = !this.loading;
var that = this,
hostname = window.location.hostname,
protocol = window.location.protocol,
port = window.location.port,
baseURL = [protocol,'//', hostname,':', port, '/articles'].join('');

var params = {
url: baseURL,
method: 'POST',
data: { title: that.title, body: that.body }
}
$.ajax(params).done(function(response){
that.message = '送信完了しました';
that.show = false;
});
}
}
});
};


Controller側を修正し、saveは実行せずJSONデータを返すだけの処理に変更。(こう書くことで上記のJS側の $.ajax.done以下が実行可能になる。)


app/controllers/articles_controller.rb

class ArticlesController < ApplicationController

(他省略)
def create
# 既存の処理は一旦コメントアウトする。
render json: { success: true }
end


app/views/articles/index.html.erb

(中略)

<div id="article-form">
<button @click="show=!show" class="btn btn-info">New Article</button>
<div v-show="show">
<div>
<label for="title">Title</label>
<input type="text" v-model="title" />
</div>
<div>
<label for="body">Body</label>
<textarea v-model="body"></textarea>
</div>
<div>
<button @click="create" :class="{ disabled: loading }" class="btn btn-primary">Submit</button>
</div>
</div>
<p v-show="loading">{{ message }}</p>
</div>

実行する際はブラウザのコンソールでエラーがでていないことを確認。

また、Controller側のcreateアクションが呼ばれていることを確認するためbinding.pryを使ってみたりする。

ここまででブラウザでは下記の挙動になる。

github.gif


データベースへの登録を行う


Controllerを修正し、保存を実行。

パラメータの参照部分を修正し、save実行。

save後はページ遷移したくないので、一旦下記の書き方にしておく。


app/controllers/articles_controller.rb

class ArticlesController < ApplicationController

(他省略)
def create
@article = Article.new(title: params[:title], body: params[:body])
if @article.save
render json: { success: true }
else
render :new
end
end

これでデータベースへの登録が実装できた!

ただ、現時点では登録後の記事一覧はページを更新しないと内容が更新されないので、その点については以降のステップで行う。

また、このままでは登録後にNewArticleボタンを押すとフォームに値が残ってしまっているので、最後に空文字を代入しておく。


app/assets/javascripts/articles.js

(中略)

methods: {
create: function() {
this.loading = !this.loading;
var that = this,
hostname = window.location.hostname,
protocol = window.location.protocol,
port = window.location.port,
baseURL = [protocol,'//', hostname,':', port, '/articles'].join('');

var params = {
url: baseURL,
method: 'POST',
data: { title: that.title, body: that.body }
}
$.ajax(params).done(function(response){
that.message = '送信完了しました';
that.show = false;
that.title = ''; //title, bodyへ空文字代入
that.body = '';
});
}



記事の一覧表示の実装

下記のステップで進める。


  • 記事一覧を表示

  • 登録後に記事一覧を更新


3. 記事一覧を表示する

Railsのインスタンス変数articlesを表示している一覧部分をVue.jsで実装する。

実装の流れは下記のイメージ


  • Rails側では記事一覧をJSONデータで返すAPIを作成する。

  • JS側では上記のエンドポイントへ通信し、一覧データを取得。

  • View側ではJSで作成されたデータを描画する。


Railsで記事一覧をJSONデータで返すAPIを作成する

返ってくるJSONデータのイメージはこのような感じ。

articlesという配列に各レコードのオブジェクトが入っている。IDも取得する。

{

"articles":[
{"id":1,"title":"red","body":"apple"},
{"id":2,"title":"blue","body":"sky"},
{"id":3,"title":"yellow","body":"banana"}
]
}

Railsのjbuilderを利用し、上記のデータを返すJSONファイルを作成


app/views/articles/index.json.jbuilder

json.articles do

json.array! @articles do |article|
json.id article[:id]
json.title article[:title]
json.body article[:body]
end
end

これで、下記のエンドポイントにアクセスするとイメージ通りのJSONデータが返却されている。ブラウザで確認してみる。

スクリーンショット 2017-08-22 11.50.18.png


JavaScriptで記事一覧データを取得する

Vue.jsの beforeMountというライフサイクルフックを設定し、そのタイミングで記事一覧のJSONデータへ通信を行う。


app/assets/javascripts/articles.js

window.onload = function() {

var vm = new Vue({
el: '#article-form',
data: {
show: false,
loading: false,
message: '送信中です',
title: '',
body: ''
},
beforeMount: function () {
var that = this,
hostname = window.location.hostname,
protocol = window.location.protocol,
port = window.location.port,
baseURL = [protocol,'//', hostname,':', port, '/articles.json'].join('');

var params = {
url: baseURL,
method: 'GET'
}
$.ajax(params).done(function(response){
console.log(response);
});
},
methods: {
create: function () {
以下省略
}
}
});
};


Ajax後、取得した結果(response変数)をコンソールに表示してみて中身を確認。

配列articlesに記事一覧のデータが入っていることが確認できる!

スクリーンショット_2017-08-22_11_01_15.png


View側でJSで取得した記事一覧データを表示する


  • tableタグにIDセレクターを設定。(Vueのインスタンス生成時に必要)

  • thタグにIDを追加

  • tbodyタグ以下のデータ表示部分を書き換える。Vue.jsのv-forディレクティブを使用。


app/views/articles/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Articles</h1>

<table id="articles-list">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Body</th>
<th colspan="3"></th>
</tr>
</thead>

<tbody>
<tr v-for="article in articles">
<td>{{ article.id }}</td>
<td>{{ article.title }}</td>
<td>{{ article.body }}</td>
</tr>
</tbody>
</table>

<br>

<div id="article-form">
以下省略
</div>


Viewに合わせてJS側も修正する。


  • Vueインスタンスを記事一覧表示用と、登録用に分割して処理を書く
    (対象としているView側のIDセレクターが異なるので)

  • articles変数を配列として設定(articles: [])


app/assets/javascripts/articles.js

window.onload = function() {

var articlesListViewModel = new Vue({
el: '#articles-list',
data: {
articles: []
},

beforeMount: function () {
var that = this,
hostname = window.location.hostname,
protocol = window.location.protocol,
port = window.location.port,
baseURL = [protocol,'//', hostname,':', port, '/articles.json'].join('');

var params = {
url: baseURL,
method: 'GET'
};
$.ajax(params).done(function(response){
that.articles = response.articles;
});
}
});

var articleViewModel = new Vue({
el: '#article-form',
data: {
show: false,
loading: false,
message: '送信中です',
title: '',
body: ''
},
methods: {
create: function () {
以下省略
}
}
});
};


この状態でブラウザを更新すると、記事一覧の部分がJavascriptを使って実装できたことを確認できる。IDが表示された!

既存の登録処理も変わらずできることを確認。

スクリーンショット 2017-08-22 11.45.44.png

登録データはページを更新しないと現時点では一覧に反映されない。次ステップで実装する。


4. 登録後に記事一覧を更新する

方法自体は、JS側のcreateメソッドのAjax後にarticlesプロパティ(配列)へデータを追加するイメージ。

(下記のデータはサンプル)

$.ajax(params).done(function(response){

that.articles.push({ id: 100, title: 'test_title', body:
'test_body' });
});

これを実装するにあたり、View側で記事一覧と登録フォームの上位にdivを設定し、そのIDセレクターに対してVueインスタンスを作成し、記述する必要がある。

<div id="articles-view">を設置し、一覧のtableタグ、登録フォームのdivタグのidの設定は不要になるので削除。


app/views/articles/index.html.erb

<div id="articles-view" class="container">

<p id="notice"><%= notice %></p>
<h1>Articles</h1>

<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Body</th>
<th colspan="3"></th>
</tr>
</thead>

<tbody>
<tr v-for="article in articles">
<td>{{ article.id }}</td>
<td>{{ article.title }}</td>
<td>{{ article.body }}</td>
</tr>
</tbody>
</table>

<br>

<div>
<button @click="show=!show" class="btn btn-info">New Article</button>
<div v-show="show">
<div>
<label for="title">Title</label>
<input type="text" v-model="title" />
</div>
<div>
<label for="body">Body</label>
<textarea v-model="body"></textarea>
</div>
<div>
<button @click="create" :class="{ disabled: loading }" class="btn btn-primary">Submit</button>
</div>
</div>
<p v-show="loading">{{ message }}</p>
</div>
</div>


Controllerを修正し、登録完了後に登録したデータを返すように設定。


app/controllers/articles_controller.rb

(中略)

def create
@article = Article.new(title: params[:title], body: params[:body])

if @article.save
render json: @article
else
render :new
end
end


統合したVueインスタンスを作成し、処理をまとめる。

登録完了後に記事一覧の配列へ登録データをpushする


app/assets/javascripts/articles.js

window.onload = function() {

var articlesListViewModel = new Vue({
el: '#articles-view',
data: {
articles: [],
show: false,
loading: false,
message: '送信中です',
title: '',
body: ''
},
beforeMount: function () {
var that = this,
hostname = window.location.hostname,
protocol = window.location.protocol,
port = window.location.port,
baseURL = [protocol,'//', hostname,':', port, '/articles.json'].join('');

var params = {
url: baseURL,
method: 'GET'
};
$.ajax(params).done(function(response){
that.articles = response.articles;
});
},
methods: {
create: function () {
this.loading = !this.loading;
var that = this,
hostname = window.location.hostname,
protocol = window.location.protocol,
port = window.location.port,
baseURL = [protocol,'//', hostname,':', port, '/articles'].join('');

var params = {
url: baseURL,
method: 'POST',
data: { title: that.title, body: that.body }
};
$.ajax(params).done(function(response){
that.articles.push({ id: response.id, title:
response.title, body: response.body });
that.message = '送信完了しました';
that.show = false;
that.title = '';
that.body = '';
});
}
}
});
};


これで登録後、データが一覧に反映されることを確認!

github.gif


その他の実装


通信部分を別ファイルへ切り出す

実装自体は完成しているが、通信部分が冗長なので別ファイルへ切り出してすっきりさせる。

$ mkdir app/assets/javascripts/models

$ touch app/assets/javascripts/models/articleModel.js


app/assets/javascripts/models/articleModel.js

var ArticleModel = (function () {

function ArticleModel() {
var hostname = window.location.hostname,
protocol = window.location.protocol,
port = window.location.port;
this.baseURL = [protocol, '//', hostname, ':', port].join('');
}

ArticleModel.prototype = {
index : function () {
var params = {
url: this.baseURL + '/articles.json',
type: "GET",
action: "index"
};
return this._request(params);
},
create : function (data) {
var params = {
url: this.baseURL + '/articles',
type: "POST",
data: { title: data['title'], body: data['body'] },
action: "create"
};
return this._request(params);
},
_request : function (params) {
this.deferred = $.ajax(params);
}
};
return ArticleModel;
}());



app/assets/javascripts/articles.js

window.onload = function() {

var articleModel = new ArticleModel();
var articlesListViewModel = new Vue({
el: '#articles-view',
data: {
articles: [],
show: false,
loading: false,
message: '送信中です',
title: '',
body: ''
},
beforeMount: function () {
var that = this;
articleModel.index();
articleModel.deferred.done(function(response) {
that.articles = response.articles;
});
},
methods: {
create: function () {
this.loading = !this.loading;
var that = this,
data = { title: that.title, body: that.body };
articleModel.create(data);
articleModel.deferred.done(function(response) {
that.articles.push({ id: response.id, title: response.title, body: response.body });
that.message = '送信完了しました';
that.show = false;
that.title = '';
that.body = '';
});
}
}
});
};

これで修正前と同様の動きができる確認する。


入力内容が正しく登録されているかチェックを追加する


app/assets/javascripts/articles.js

// createメソッド以外は省略

methods: {
create: function () {
this.loading = !this.loading;
var that = this,
data = { title: that.title, body: that.body };
articleModel.create(data);
articleModel.deferred.done(function(response) {
if (that.title === response.title && that.body === response.body){
that.message = '登録しました';
that.articles.push({ id: response.id, title: response.title, body: response.body });
} else {
that.message = '正しく登録できませんでした';
}
that.show = false;
that.title = '';
that.body = '';
});
}

これでメッセージの表示が正しく出来るか確認する。


Vue.jsのコードが直に表示されることを防ぐ

ページ読み込み時などView側でVue.jsのコードが直に表示されてしまうことがあるので、それを防ぐためにView側とcssにv-cloakを追加する


app/views/articles/index.html.erb

<div id="articles-view" v-cloak class="container">

(省略)


app/assets/stylesheets/scaffolds.scss

[v-cloak] {

display: none;
}


まとめ

実際にやってみて、Railsではコマンド一つでできていたためか、Vue.jsでは各ステップがボリュームが大きく感じ、書き換えるのは難しかった。

フロントエンド特有の思考に切り替える必要があり、簡単そうな事でも実装方法が思いつかない事が多く悩んだ〜(。´・д・)

とはいえ、使っていく過程でだんだんVue.jsのお作法に慣れてきて、考え方はシンプルなので入って行きやすかった。

(きっとコンポーネントに手を出し時始めるとさらに複雑になるんだろうな。とかとか。)

引き続き学習を進めて慣れていこうと思います〜v(。・ω・。)

(記事の編集や削除も追って追加予定です!)