はじめに
Rails で作る管理画面等でリッチな動きをつけたいと思った場合、何かしらの JavaScript フレームワーク の利用を検討するかと思います。
以前に [APIサーバを Rails、フロントエンドを AngularJS で開発する [その①]] (http://qiita.com/hkusu/items/da51fb9aaea1d490e81f) [その②] [その③] という投稿をしたのですが、このような感じで Rails は APIサーバに徹する のが良いのかな、と個人的には思うものの、JavaScript フレームワークを View で薄く使う という構成もあるのかなと。(jQuery を触るよりましだよね、という程度。)
今回は、JavaScirptフレームワーク ⇔ Rails 間で、API 経由ではなく View 経由でデータをやりとり する、ということをやってみました。
Knockout.js を利用していますが、素の JavaScript でも他のフレームワークでも、考え方は同じかと思います。
前提
- フロントエンドの JavaScript フレームワークとして Knockout.js を利用します。
- これがベストかどうか分りませんが、枯れた薄い View フレームワークであるため。
- いまだと Vue.js とかの方がいいのかもしれないとも思いつつ。
- これがベストかどうか分りませんが、枯れた薄い View フレームワークであるため。
- 私の手元の環境はこちら。
成果物
イメージがつきやすいよう、先に示します。
① 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 オブジェクトです。
$ ->
##############################
# 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 記法です。
%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
を自動的に含めることが出来ます。
// エラー表示エリア
- 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) には則らない?
- ただ、こちらの記事(Railsが時代に合わなくなってきた)にあるように、Rails の JavaScript に対するサポートが薄いので、仕方がない気もする..
気が向いたら Vue.js バージョンを書いてみようと思います。