LoginSignup
7
7

More than 5 years have passed since last update.

Rails 4 で Knockout.js の ViewModel を Controller毎に別のファイルで管理する

Last updated at Posted at 2014-08-28

少しハマったので、問題と解決方法をメモします。
これがベストなのか良く分からないので、もし別案があったらコメントお願いします。

やりたいこと

各 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 がバインドされることになるが、次のような重複エラーとなる。

    スクリーンショット 2014-08-28 21.49.28.png

検討

  • 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'"}

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が存在したらバインドする、としてやります。

これで安全に、本来目的とする 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に対応する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
    
7
7
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
7
7