54
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

リッチなUIを実現する為に。Rails⇔JavaScriptフレームワーク間でデータを受け渡す方法

Posted at

はじめに

Rails で作る管理画面等でリッチな動きをつけたいと思った場合、何かしらの JavaScript フレームワーク の利用を検討するかと思います。

以前に [APIサーバを Rails、フロントエンドを AngularJS で開発する [その①]] (http://qiita.com/hkusu/items/da51fb9aaea1d490e81f) [その②] [その③] という投稿をしたのですが、このような感じで Rails は APIサーバに徹する のが良いのかな、と個人的には思うものの、JavaScript フレームワークを View で薄く使う という構成もあるのかなと。(jQuery を触るよりましだよね、という程度。)

今回は、JavaScirptフレームワーク ⇔ Rails 間で、API 経由ではなく View 経由でデータをやりとり する、ということをやってみました。

Knockout.js を利用していますが、素の JavaScript でも他のフレームワークでも、考え方は同じかと思います。

前提

成果物

イメージがつきやすいよう、先に示します。

① Railsの一覧(index)画面

Knockout.js でデータバインド & 描写しています。

ff01e274-31dc-4e97-558d-7c4fd16200f0.png

② Railsの新規(new)画面

Knockout.js でデータバインドしています。

863def8a-9386-3ba8-c048-00cd10673f87.png

ソースコード

前提として、次のように Scaffold したものに対して手を加えています。

$ ./bin/rails g scaffold person name:string age:integer memo:text
$ ./bin/rake db:migrate

① Knockout.js の ViewModel

Rails では Controller 毎に JavaScript(CoffeeScript) ファイルが1つ作成されるので、そのファイルに Knockout.js の ViewModel を定義します。
(説明を簡略化するために、実装したのは 一覧/新規 画面のみです。)

この例では、画面 1つ に対して、1つの ViewModel を定義しています。
(複雑な UI であれば、複数の ViewModel を定義して良いと思います。)

ちなみに、バインド対象(Knockout.jsの監視対象)の変数は、ソースコード中の @XXX (ViewModel の this) で指定したものです。
また、ソースコード中の rails.objPeople オブジェクトは、後述の Rails の View で定義された JavaScript オブジェクトです。

app/assets/javascripts/people.js.coffee
$ ->

  ##############################
  # index ページ用の ViewModel
  ##############################
  IndexViewModel = () ->
    # Knockout.JS の 監視下へ
    @items = ko.observableArray rails.objPeople
    return

  # ViewModel を View へバインド
  bindScope = "#IndexView"
  if $(bindScope)[0] # 本 JS ファイルは Rails のどの View でも読み込まれてしまう為、div タグでバインドするか否かを決定する
    ko.applyBindings new IndexViewModel(), $(bindScope)[0] # バインドする div エリアを明示的に指定する

  ##############################
  # form ページ用の ViewModel
  ##############################
  FormViewModel = () ->
    @name = ko.observable ""
    @age = ko.observable ""
    @memo = ko.observable ""
    return

  bindScope = "#FormView"
  if $(bindScope)[0]
    ko.applyBindings new FormViewModel(), $(bindScope)[0]

  return

② Rails の View (一覧ページ)

表示系のページのポイントは どうやって Rails から JavaScript へデータを渡すか になるかと思います。

rails.objPeople= #{raw @people.to_json} のようにして、Rails が View に展開したデータを JavaScript オブジェクト として受け取ります。
(Global だと気持ち悪いので、念のため名前空間 rails を定義。)

こちらを参考にさせて頂きました ⇒ RailsからJavaScriptにデータを渡す

こうすることにより、Rails から出力したデータを、Knockout.js の ViewModel から参照することが出来ます。あとは普通に Knockout.js で View を適当に作るだけです。

※ 下記のコードは読みづらいかもしれませんが Haml 記法です。

app/views/people/index.html.haml
%h1 Listing people

#IndexView

  // 方針としては、なるべく Rails のタグ生成機能は使わない。デザイナーさんが扱いにくい為。

  :coffee
    # Rails から取得したJSONデータを、JS の グローバル変数 rails の要素としてエクスポート
    rails = window.rails = window.rails ? {}
    rails.objPeople= #{raw @people.to_json}

  %table.table.table-striped{"data-bind" => "visible: items().length > 0"}
    %thead
      %tr
        %th 名前
        %th 年齢
        %th メモ
        %th
        %th
        %th
    %tbody{"data-bind" => "foreach: items"}
      %tr
        %td{"data-bind" => "text: $data.name"}
        %td{"data-bind" => "text: $data.age"}
        %td{"data-bind" => "text: $data.memo"}
        %td
          %a{"data-bind" => "attr: { href: '/people/' + $data.id }"} Show
        %td
          %a{"data-bind" => "attr: { href: '/people/' + $data.id + '/edit' }"} Edit
        %td
          %a{"data-confirm" => "削除しますか?", "data-method" => "delete", "data-bind" => "attr: { href: '/people/' + $data.id }"} Destroy

  %P
    %a{:href => "/people/new"} New Person

ちなみに JavaScript(CoffeeScript)部分は、Rails の View 描写時に次のように展開されます。JavaScript オブジェクトにデータが格納されているのが分るかと思います。

スクリーンショット 2014-10-08 12.03.42.png

③ Rails の View (新規ページ)

登録系系のページのポイントは どうやって JavaScript から Rails へデータを渡すか になるかと思います。方法としては、Rails の 隠し Form 要素に変数をバインド してやります。
(下記のソースコードでいうと、一番下のあたりです。)

Rails の規約により、POST するキー名は モデル名[カラム名] とする必要があります。
また、Rails の form_tag を使うことで、POST リクエストに authenticity_token を自動的に含めることが出来ます。

app/views/people/_form.html.haml
// エラー表示エリア
- if @person.errors.any?
  #error_explanation
    %h2= "#{pluralize(@person.errors.count, "error")} prohibited this person from being saved:"
    %ul
      - @person.errors.full_messages.each do |msg|
        %li= msg

#FormView

  .form-group
    名前
    %input.form-control{:type => "text", "data-bind" => "value: name, valueUpdate: 'afterkeydown'"}
  .form-group
    年齢
    %input.form-control{:type => "number", "data-bind" => "value: age, valueUpdate: 'afterkeydown'"}
  .form-group
    メモ
    %textarea.form-control{"data-bind" => "value: memo, valueUpdate: 'afterkeydown'"}

  // POST するデータを保持するエリア。値はデータバインドで入力される
  = form_tag('/people') do
    %input{:type => "hidden", :name => "person[name]", "data-bind" => "value: name"}
    %input{:type => "hidden", :name => "person[age]", "data-bind" => "value: age"}
    %input{:type => "hidden", :name => "person[memo]", "data-bind" => "value: memo"}
    %input.btn.btn-primary{:type => "submit", :value => "保存", "data-confirm" => "保存しますか?"}

ちなみに Form部分は、Rails の View 描写時に次のように展開されます。

スクリーンショット 2014-10-08 12.06.47.png

おわりに

今回の成果物だと、単に Rails の 一覧/新規 ページを置き換えただけなので、Knockout.js を利用することによる恩恵は感じられないかもしれませんが、バインドされたデータを利用して UI をごりごり出来ます。

更新系は View 経由でなく API で連携する、という方針でもいいかもしれません。
(画面遷移なしにDBデータを更新したい場合など。)

気になることとしては、

  • テストという観点では、この構成はどうなんだろう..??
  • Rails の DRY (Don't Repeat Yourself) には則らない?

気が向いたら Vue.js バージョンを書いてみようと思います。

ほか参考記事:

54
55
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
54
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?