前提
普段はRuby on Railsを使いつつ、フロントエンド強化のため、Vue.jsを学習中。
Railsで実装したアプリケーションをVue.jsを使って作り変えるをお題に、シンプルなサンプルを作成します。
Vue.jsを使って極力ページ遷移しないアプリケーションを作っていきます。
やること
-
簡易的なブログ機能を想定したアプリケーションをイメージしてRailsのScaffodコマンドでベースになる部分を作成。
-
登録と一覧表示処理について画面遷移しない形になるようにVue.jsを利用してその機能を実現。
Vue.js初心者向けの内容になります。
サンプルアプリはこちら
下準備
Railsで土台を作成する
Scaffoldコマンドでブログ記事管理用(Article)のModel, View, Controllerを作成。
Modelにはタイトルと本文用のカラムを作成。
$ rails g scaffold article title body:text
データベース作成、migration実行
$ bin/rails db:create
$ bin/rails db:migrate
RailsでできたこのアプリをVue.jsを使う形に編集していく〜!
Vue.jsのインストール
Vue.js公式サイトからソースファイルをダウンロードして使用する。開発バージョンを選択。
app/assets/javascriptsに配置し、application.jsで読み込む。
//= require jquery
//= require jquery_ujs
//= require turbolinks
+ //= require vue
//= require_tree .
ブラウザでVue.jsが読み込まれていることを確認
Chromeの場合、右クリック 検証 > ConsoleタブでVue.jsが読み込まれているメッセージが表示されていればOK。
記事の新規登録の実装
下記のステップで進める。
- 新規作成フォームを一覧ページに表示する
- 記事を登録する
1. 新規作成フォームを表示する
Railsのform_for
を使う形ではなく、HTMLのタグを使ってフォームを描画。
Railsでは最低限のことを行い、Javascriptで実装する!
実装イメージ
New Article ボタンを押下することで、フォームの表示/非表示が切り替えれるようにする。
一覧表示用のView, JavaScriptファイルを編集
<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
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側のフォーム要素の上位divにv-show="show"
を設定。これにより、JS側で与えているdataオブジェクトのshow
の値がtrueの場合のみdivを表示する。
v-showについて
- ボタンにイベントハンドラを設定
Vuew側のNew Articleリンクを削除し、buttonタグを設置
buttonタグにイベントハンドラとしてv-on:click
を設定(@click
は省略記法)
@clickでshow
の値がtrue/falseと切り変わるように記述。
JS側でshowのデフォルト値にfalseに設定しているので通常時は表示されない。
v-on:clickについて
2. 記事を登録する
フォームの入力値をJS側で受け取る
View側のtitle, bodyの入力欄にv-model
を設定。v-modelについて。
Submitボタンにメソッドを実行するイベントハンドラ@click
を設定。
(中略)
<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を定義し、メソッド内で受け取ったものをコンソールに表示してみる。
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を追加し実装する。
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
以下が実行可能になる。)
class ArticlesController < ApplicationController
(他省略)
def create
# 既存の処理は一旦コメントアウトする。
render json: { success: true }
end
(中略)
<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
を使ってみたりする。
ここまででブラウザでは下記の挙動になる。
データベースへの登録を行う
Controllerを修正し、保存を実行。
パラメータの参照部分を修正し、save実行。
save後はページ遷移したくないので、一旦下記の書き方にしておく。
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ボタンを押すとフォームに値が残ってしまっているので、最後に空文字を代入しておく。
(中略)
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ファイルを作成
json.articles do
json.array! @articles do |article|
json.id article[:id]
json.title article[:title]
json.body article[:body]
end
end
これで、下記のエンドポイントにアクセスするとイメージ通りのJSONデータが返却されている。ブラウザで確認してみる。
JavaScriptで記事一覧データを取得する
Vue.jsの beforeMountというライフサイクルフックを設定し、そのタイミングで記事一覧のJSONデータへ通信を行う。
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に記事一覧のデータが入っていることが確認できる!
View側でJSで取得した記事一覧データを表示する
- tableタグにIDセレクターを設定。(Vueのインスタンス生成時に必要)
- thタグにIDを追加
- tbodyタグ以下のデータ表示部分を書き換える。Vue.jsのv-forディレクティブを使用。
<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: [])
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が表示された!
既存の登録処理も変わらずできることを確認。
登録データはページを更新しないと現時点では一覧に反映されない。次ステップで実装する。
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の設定は不要になるので削除。
<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を修正し、登録完了後に登録したデータを返すように設定。
(中略)
def create
@article = Article.new(title: params[:title], body: params[:body])
if @article.save
render json: @article
else
render :new
end
end
統合したVueインスタンスを作成し、処理をまとめる。
登録完了後に記事一覧の配列へ登録データをpushする。
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 = '';
});
}
}
});
};
これで登録後、データが一覧に反映されることを確認!
その他の実装
通信部分を別ファイルへ切り出す
実装自体は完成しているが、通信部分が冗長なので別ファイルへ切り出してすっきりさせる。
$ mkdir app/assets/javascripts/models
$ touch 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;
}());
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 = '';
});
}
}
});
};
これで修正前と同様の動きができる確認する。
入力内容が正しく登録されているかチェックを追加する
// 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
を追加する
<div id="articles-view" v-cloak class="container">
(省略)
[v-cloak] {
display: none;
}
まとめ
実際にやってみて、Railsではコマンド一つでできていたためか、Vue.jsでは各ステップがボリュームが大きく感じ、書き換えるのは難しかった。
フロントエンド特有の思考に切り替える必要があり、簡単そうな事でも実装方法が思いつかない事が多く悩んだ〜(。´・д・)
とはいえ、使っていく過程でだんだんVue.jsのお作法に慣れてきて、考え方はシンプルなので入って行きやすかった。
(きっとコンポーネントに手を出し時始めるとさらに複雑になるんだろうな。とかとか。)
引き続き学習を進めて慣れていこうと思います〜v(。・ω・。)
(記事の編集や削除も追って追加予定です!)