LoginSignup
21
27

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-08-23

前提

普段は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(。・ω・。)
(記事の編集や削除も追って追加予定です!)

21
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
27