少しハマったので、問題と解決方法をメモします。
これがベストなのか良く分からないので、もし別案があったらコメントお願いします。
やりたいこと
各 View に対する Knockout.js の ロジック(ViewModel)を、Controller毎に別の JavaScirpt(CoffeeScript)ファイルに書きたい。
.. いたって普通のことなのですが、Rails だと下記のような問題がありました。
問題
環境は Rails 4.1.1
、Knockout.js 3.1.0
です。
-
Controller毎のJavaScript(CoffeeScript)ファイルは、
app/assets/javascipts
配下に置けるものの、Railsの仕様上、全ページでロードされしまう。 -
ko.applyBindings
は引数の2つ目で、バインド対象の DOM を指定する機能がある。しかし、その DOM が存在しない場合は、全 document がバインド対象となる、という挙動である。 -
よって、本来目的とする View と別の View で
ko.applyBindings
が呼び出された場合は、一つの DOM に複数の ViewModel がバインドされることになるが、次のような重複エラーとなる。
検討
- View ファイルに JavaScipt(CoffeeScirpt) を書けばなんとかなる?
- ⇒ 管理上、やっぱり View とは分けたい。
- アセットパイプラインあたりの設定を変えたり、各Viewそれぞれで必要な JavaScript を読み込むようにする?
- ⇒ 面倒だし検証が必要。Sprockets の動きや、プレコンパイルの動きも良く分からない..
- ⇒ なにより、View 毎に読み込む JavaScript(CoffeeScript) ファイルが異なるとなると、production 時の JavaScript ファイル群の concat(結合) の恩恵が受けられない。
どうしたか
Railsの設定はいじらず、本来目的とする View以外で Knockout.js のロジックが呼ばれても問題ないようにします。(全ページでロードされても問題ないようにする。)
ちなみに、私の環境(Mac)は次のとおりです。
OS:
Mac OS X 10.9.4 (Mavericks)
Rails:4.1.1
Ruby:2.1.2
Knockout.js:3.1.0
View
-
Knockout.js のバインド範囲は、明示的に div タグ id 指定をするようにします。
- 下記は Haml 記法ですが、
<div id="ItemView">
と</div>
で囲うという意味です。
app/views/items/index.html.haml〜 #ItemView %p ほげほげ: %input{"data-bind" => "value: hoge, valueUpdate: 'afterkeydown'"} 〜
- 下記は Haml 記法ですが、
Viewに対応するJavaScript(CoffeeScript)ファイル
-
ko.applyBindings
の際、2つ目の引数で明示的に、対応する div タグ id を指定します。app/assets/javascripts/items.js.coffee$ -> 〜 viewScope = "#ItemView" if $(viewScope)[0] ko.applyBindings new ItemViewModel(), $(viewScope)[0] return
-
if
の説明- このJavaScript(CoffeeScript)ファイルは、Railsの全ページで読み込まれてしまいます。その際、別ページの View ではもちろん、
<div id="ItemView">
は存在しません。そのままだと、ko.applyBindings
引数2つ目の DOM が存在しない場合は、全 document がバインド対象となる という挙動により、別ページで本来やりたかったバインドとこのバインドが重複し、例のエラーとなってしまいます。 - そこで、上記のように
if
で、もしその DOMが存在したらバインドする、としてやります。
- このJavaScript(CoffeeScript)ファイルは、Railsの全ページで読み込まれてしまいます。その際、別ページの View ではもちろん、
これで安全に、本来目的とする View に ViewModel のバインドができ、Controller 毎のロジックをそれぞれ独立した JavaScript(CoffeeScript)ファイルで管理し、また production 時には JavaScriptファイル郡の minijy や concat(結合)の恩恵を受けることができる構成になるかと思います。
[2014.08.29追記]どうしたか その②
同僚に相談したところ、こっちの方がすっきり書けそうなので紹介します。
View
-
View側で次のようにバインディングします。ViewModel名の前に
HOGEAPP_ITEM.
をつけておきます。- Viewから
(function(){
..}).call(this);
で囲われた JavaScriptファイルの中身にアクセスするため。
app/views/items/index.html.haml〜 #ItemView %p ほげほげ: %input{"data-bind" => "value: hoge, valueUpdate: 'afterkeydown'"} :coffee $ -> ko.applyBindings new HOGEAPP_ITEM.ItemViewModel(), $("#ItemView")[0] 〜
- Viewから
Viewに対応するJavaScript(CoffeeScript)ファイル
-
次のようにして、ViewModelを外部へ公開します。
- 間違えて他のView用のViewModel呼ぶのを防ぐため、
_ITEM
のように Controller名をつけておきます。
app/assets/javascripts/items.js.coffee$ -> HOGEAPP_ITEM = window.CPADM_ITEM = window.CPADM_ITEM ? {} HOGEAPP_ITEM.ItemViewModel = () -> self = hoge: ko.observable() moge: -> return self return
- 間違えて他のView用のViewModel呼ぶのを防ぐため、