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

# 前提
普段はRuby on Railsを使いつつ、フロントエンド強化のため、Vue.jsを学習中。
**Railsで実装したアプリケーションをVue.jsを使って作り変える**をお題に、シンプルなサンプルを作成します。
Vue.jsを使って**極力ページ遷移しないアプリケーション**を作っていきます。

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

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

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

### サンプルアプリはこちら
[vue-js-articles](https://github.com/kaorina/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](https://qiita-image-store.s3.amazonaws.com/0/111637/cb00e93b-cda6-b00d-e839-0c213362cc61.png)

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

## Vue.jsのインストール
[Vue.js公式サイト](https://jp.vuejs.org/v2/guide/installation.html)からソースファイルをダウンロードして使用する。開発バージョンを選択。

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

```javascript: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](https://qiita-image-store.s3.amazonaws.com/0/111637/2e2de261-e910-ad42-1f35-237356aee677.png)

# 記事の新規登録の実装
下記のステップで進める。

- 新規作成フォームを一覧ページに表示する
- 記事を登録する

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

Railsの`form_for`を使う形ではなく、HTMLのタグを使ってフォームを描画。
**Railsでは最低限のことを行い、Javascriptで実装する！**

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

![github.gif](https://qiita-image-store.s3.amazonaws.com/0/111637/bfc4f32b-f186-e2a8-f3a1-4c7f646b2af5.gif)

## 一覧表示用のView, JavaScriptファイルを編集
```erb: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
```

```javascript: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`を作成。
<br>

- 新規登録用のフォーム設置
View側で新規作成用のフォーム要素を設置。**HTMLのタグ**（今回は`input, textarea`）を使って追加。
<br>

- フォームにv-showディレクティブを設定
View側の**フォーム要素の上位div**に`v-show="show"`を設定。これにより、JS側で与えているdataオブジェクトの`show`の値がtrueの場合のみdivを表示する。
[v-showについて](https://jp.vuejs.org/v2/guide/conditional.html#v-show)
<br>

- ボタンにイベントハンドラを設定
Vuew側のNew Articleリンクを削除し、buttonタグを設置
buttonタグにイベントハンドラとして`v-on:click`を設定（`@click`は省略記法)
@clickで`show`の値がtrue/falseと切り変わるように記述。
JS側でshowのデフォルト値にfalseに設定しているので通常時は表示されない。
[v-on:clickについて](https://jp.vuejs.org/v2/guide/events.html)

# 2. 記事を登録する
## フォームの入力値をJS側で受け取る
View側のtitle, bodyの入力欄に`v-model`を設定。[v-modelについて。](https://jp.vuejs.org/v2/guide/forms.html)
Submitボタンにメソッドを実行するイベントハンドラ`@click`を設定。

```erb: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を定義し、メソッド内で受け取ったものをコンソールに表示してみる。

```javascript: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を追加し実装する。

```javascript: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`以下が実行可能になる。）

```ruby:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  （他省略）
  def create
    # 既存の処理は一旦コメントアウトする。
    render json: { success: true }
  end
```

```erb: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](https://qiita-image-store.s3.amazonaws.com/0/111637/32c017ad-5b86-9855-3650-5b55c2052ae0.gif)

## データベースへの登録を行う
### Controllerを修正し、保存を実行。
パラメータの参照部分を修正し、save実行。
save後はページ遷移したくないので、一旦下記の書き方にしておく。

```ruby: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ボタンを押すとフォームに値が残ってしまっているので、最後に空文字を代入しておく。

```javascript: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ファイルを作成


```erb: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](https://qiita-image-store.s3.amazonaws.com/0/111637/4a835602-7c46-f57f-ca16-e02764d66587.png)


## JavaScriptで記事一覧データを取得する
Vue.jsの [beforeMountというライフサイクルフック](https://jp.vuejs.org/v2/guide/instance.html)を設定し、そのタイミングで記事一覧のJSONデータへ通信を行う。

```js: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](https://qiita-image-store.s3.amazonaws.com/0/111637/7037af57-9bae-f84e-bc1c-4da048d2a71b.png)

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

- tableタグにIDセレクターを設定。（Vueのインスタンス生成時に必要）
- thタグにIDを追加
- tbodyタグ以下のデータ表示部分を書き換える。Vue.jsの[v-forディレクティブ](https://jp.vuejs.org/v2/guide/list.html#v-for)を使用。

```erb: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: [])

```js: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](https://qiita-image-store.s3.amazonaws.com/0/111637/64d6cc5d-aa77-365d-349d-db02c4efd1e1.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の設定は不要になるので削除。

```erb: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を修正し、登録完了後に登録したデータを返すように設定。

```ruby: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する**。

```js: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](https://qiita-image-store.s3.amazonaws.com/0/111637/758c227d-1fb5-a05f-2a7a-c9ba0c8432d9.gif)

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

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

```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;
}());
```

```js: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 = '';
        });
      }
    }
  });
};
```
これで修正前と同様の動きができる確認する。

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

```js: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`を追加する

```js:app/views/articles/index.html.erb
<div id="articles-view" v-cloak class="container">
(省略)
```

```scss:app/assets/stylesheets/scaffolds.scss
[v-cloak] {
  display: none;
}
```

# まとめ
実際にやってみて、Railsではコマンド一つでできていたためか、Vue.jsでは各ステップがボリュームが大きく感じ、書き換えるのは難しかった。
フロントエンド特有の思考に切り替える必要があり、簡単そうな事でも実装方法が思いつかない事が多く悩んだ〜(｡´･д･)

とはいえ、使っていく過程でだんだんVue.jsのお作法に慣れてきて、考え方はシンプルなので入って行きやすかった。
（きっとコンポーネントに手を出し時始めるとさらに複雑になるんだろうな。とかとか。）
引き続き学習を進めて慣れていこうと思います〜v(｡･ω･｡)
（記事の編集や削除も追って追加予定です！）