Posted at

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

More than 3 years have passed since last update.


はじめに

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

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

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

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


前提


成果物

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


① Railsの一覧(index)画面

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


② Railsの新規(new)画面

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


ソースコード

前提として、次のように 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 オブジェクトにデータが格納されているのが分るかと思います。


③ 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 描写時に次のように展開されます。


おわりに

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

更新系は View 経由でなく API で連携する、という方針でもいいかもしれません。

(画面遷移なしにDBデータを更新したい場合など。)

気になることとしては、


  • テストという観点では、この構成はどうなんだろう..??

  • Rails の DRY (Don't Repeat Yourself) には則らない?



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


ほか参考記事: