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


# はじめに

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

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

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

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

# 前提

- フロントエンドの JavaScript フレームワークとして **Knockout.js** を利用します。
	- これがベストかどうか分りませんが、枯れた薄い View フレームワークであるため。
		- いまだと **Vue.js** とかの方がいいのかもしれないとも思いつつ。
- 私の手元の環境はこちら。
	- [Rails4 で Knockout ＆ Bootstrap ＆ Coffee ＆ SASS ＆ Haml な環境を構築する](http://qiita.com/hkusu/items/0c68e294521b7e5caf55)

# 成果物

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

### ① Railsの一覧(index)画面

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

![ff01e274-31dc-4e97-558d-7c4fd16200f0.png](https://qiita-image-store.s3.amazonaws.com/0/35733/d5a90b5c-1884-76c5-a235-0b0ed042515f.png "ff01e274-31dc-4e97-558d-7c4fd16200f0.png")

### ② Railsの新規(new)画面

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

![863def8a-9386-3ba8-c048-00cd10673f87.png](https://qiita-image-store.s3.amazonaws.com/0/35733/289f7b87-e532-efd3-23ab-031d051c280a.png "863def8a-9386-3ba8-c048-00cd10673f87.png")

# ソースコード

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

```shell:
$ ./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 オブジェクトです。

```coffeescript: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にデータを渡す](http://qiita.com/mosa_siru/items/e69eee47a183b13cfb62)

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

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

```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](https://qiita-image-store.s3.amazonaws.com/0/35733/aee911e8-819b-0deb-22ae-c00398d5c94b.png "スクリーンショット 2014-10-08 12.03.42.png")


## ③ Rails の View (新規ページ)

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

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

```haml: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](https://qiita-image-store.s3.amazonaws.com/0/35733/6a67a2ba-3bfe-f336-b6ec-e058f50f943c.png "スクリーンショット 2014-10-08 12.06.47.png")

# おわりに

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

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

気になることとしては、

- テストという観点では、この構成はどうなんだろう..？？
- Rails の **DRY** (Don't Repeat Yourself) には則らない？
	- ただ、こちらの記事（[Railsが時代に合わなくなってきた](http://qiita.com/kaiinui/items/2781219340d427543d08)）にあるように、Rails の JavaScript に対するサポートが薄いので、仕方がない気もする..

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

### ほか参考記事：

- [Rails 4 で Knockout.js を利用する手順](http://qiita.com/hkusu/items/9227a266ae687a3892c2)
- [Rails 4 で Knockout.js の ViewModel を Controller毎に別のファイルで管理する](http://qiita.com/hkusu/items/38fa63e851c2d7a90f2a)