Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Railsで作られた管理画面にVue.jsを導入した話

Studyplusのweb版を担当していた久保です。 最近はRailsを触ったりしています。

今回は社内向けの管理画面を作る際に、どうしても動的にDOMを操作する必要があったのでjQueryの代わりにVue.jsを導入してみました。

なぜVue.jsを選んだのか

  • Railsが生成したhtmlをテンプレートとして使うことができる
  • Rails5系以降であれば、webpackerを利用するだけで良いので導入が楽

導入方法

導入時の環境は以下

  • rails: 5.2.0
  • ruby: ruby 2.5.1p57
  • node: v8.11.2
  • yarn: 1.7.0
  • webpacker: v3.5.3

webpacker

gem 'webpacker'
$ bundle install
$ bundle exec rails webpacker:install

Vue.js

$ bundle exec rails webpacker:install:vue

Vue.jsを動かす

webpacker.ymlに記載されていますが、標準だと、app/javascript/packs配下のファイルがエントリーファイルです。

JavaScriptファイルの読み込み

例えば app/javascript/packs/hello-world.js とした場合、ヘルパーメソッドを使って簡単に読み込むことができます。

...
  <%= javascript_pack_tag 'hello-world', 'data-turbolinks-track': 'reload', defer: true %>
</head>
...

今回は app/javascript/components 配下にVue.jsのコンポーネントを何個か定義して、エントリーファイル側でimportする形で実装しました。

開発環境で動かす

bin/webpack-dev-server というファイルができており、これを使うと JavaScript を編集した際にビルドが走りブラウザが勝手にリロードされるようになります。

web: rails s -p 3000
client: sh -c 'rm -rf public/packs/* || true && bin/webpack-dev-server'

みたいなファイルを用意して、foreman を追加し

$ bundle exec foreman start -f Procfile.dev-server

とすると良いかもしれません。

本番環境

細かいチューニングが必要な場合は webpack の設定を弄るべきかもしれませんが、特に何もしなくても assets-precompile 時にいい感じになります。ここが本当に楽で素晴らしいと思っています。

Railsで生成したフォームの変化を検知して動的にDOMを操作する

Vue.jsを採用した理由として

Railsが生成したhtmlをテンプレートとして使うことができる

と書きましたが、簡単にその機能の紹介をします。

【サンプル】select要素の変更に応じてcss classの付け外しを行う

以下が設定の一部とマウント対象のerbです。

const ConversionType = {
    el: '#js-conversion_type',
    data: {
        form: {
            conversionType: document.getElementsByClassName('js-conversion_type')[0].value
        }
    },
    computed: {
        isRequired: function() {
            switch (this.form.conversionType) {
                case 'nop':
                    return {
                        title: false,
                        urlIOS: false,
                        urlAndroid: false,
                    };
                // 以下case文省略
            }
        }
    }
}

export default ConversionType;
  <div id="js-conversion_type">
    <div class="row mb-4">
      <div class="col-xs-3">
        <%= form.label :conversion_type %>
      </div>
      <div class="col-xs-9">
        <%= form.select :conversion_type, MessageDraft.enum_for_selectbox(:conversion_type), {}, class: 'form-control js-conversion_type', 'v-model' => 'form.conversionType' %>
      </div>
    </div>
    <div class="row mb-4">
      <div class="col-xs-3">
        <%= form.label :button_title, 'v-bind:class' => '{required: isRequired.title}' %>
      </div>
      <div class="col-xs-9">
        <%= form.text_field :button_title, id: :message_draft_button_title, class: 'form-control', 'v-bind:disabled' => '!isRequired.title' %>
      </div>
    </div>
    <div class="row mb-4">
      <div class="col-xs-3">
        <%= form.label :button_url_ios, 'v-bind:class' => '{required: isRequired.urlIOS}' %>
      </div>
      <div class="col-xs-9">
        <%= form.text_field :button_url_ios, id: :message_draft_button_url_ios, class: 'form-control', 'v-bind:disabled' => '!isRequired.urlIOS' %>
      </div>
    </div>
    <div class="row mb-4">
      <div class="col-xs-3">
        <%= form.label :button_url_android, 'v-bind:class' => '{required: isRequired.urlAndroid}' %>
      </div>
      <div class="col-xs-9">
        <%= form.text_field :button_url_android, id: :message_draft_button_url_android, class: 'form-control', 'v-bind:disabled' => '!isRequired.urlAndroid' %>
      </div>
    </div>
  </div>

ConversionType.jsの説明

el: '#js-conversion_type'

に関してですが、 公式ドキュメント を読むと

既存の DOM 要素に Vue インスタンスを与えます。

render 関数または template オプションも存在しない場合、マウントしている DOM 要素にある HTML がテンプレートとして抽出されます。

と書いてあります。この設定により、_form.html.erb 配下を Vue.js のテンプレートとして扱うことが可能になります。

data: {
    form: {
        conversionType: document.getElementsByClassName('js-conversion_type')[0].value
    }
},

ドキュメントは こちら です。 select要素の初期値が後述する v-model を用いた場合に設定されなかったため、値を設定しています。

select要素の変更に合わせて、class等を操作する

v-model ディレクティブ と、computed プロパティ を用います。

<%= form.select :conversion_type, MessageDraft.enum_for_selectbox(:conversion_type), {}, class: 'form-control js-conversion_type', 'v-model' => 'form.conversionType' %>

と設定するだけで、onChange 等を書かずに値の変更を検知できます。

更に computed プロパティを用いて form.conversionType の値に応じて各 input 要素が必要かどうかの bool値を持つオブジェクトが算出されるように設定します。

computed: {
    isRequired: function() {
        switch (this.form.conversionType) {
            case 'nop':
                return {
                    title: false,
                    urlIOS: false,
                    urlAndroid: false,
                };
            // 以下case文省略
        }
    }
}

テンプレート側からは

<div class="row mb-4">
  <div class="col-xs-3">
    <%# isRequired.title が true の場合は required クラスが付与される %>
    <%= form.label :button_title, 'v-bind:class' => '{required: isRequired.title}' %>
  </div>
  <div class="col-xs-9">
    <%# isRequired.title が false の場合は disabled 属性が付与される %>
    <%= form.text_field :button_title, id: :message_draft_button_title, class: 'form-control', 'v-bind:disabled' => '!isRequired.title' %>
  </div>
</div>

というふうに参照できます。

まとめ

Vue.js全く触ったことなかったのですが、あくまでRailsメインで画面を組んだ時に、補助としてVue.jsを使うのは導入と学習コストの点から悪くないのではという印象を受けました。