Knockout.jsで、Ajaxで取得したhtmlに対してbindingしたい場合がありました。
うまくいったのでメモを残しておきます。
どうしてそんなことをしたくなったのか?
新規作成と編集のフォームを作りたかったのだけれど、Railsのモデルが複雑に絡んだformをKnockout.jsで制御するのはちょっと難しかったため(Knockout.jsを介してのformの投稿先、methodの変更などは面倒かなと…)、なるべくRailsの世界で完結するようにしたかったので動的にformを取ってくることにしました。
しかし、フォームの簡易的な検証機能はKnockout.jsを使いたい、という感じです。
簡単にはできない
シンプルに$.get
でformを取得してしまえば成立するかと思いましたが、これだとbindingは有効になりませんでした。
<div id="form-memo" data-binding="with: formMemoViewModel">
</div>
$.get Routes.new_memo_path(), (data) ->
$('#form-memo').html(data)
class @FormMemoViewModel
constructor: ->
# なんらかの処理
$ ->
window.appViewModel = {
formMemoViewModel: new FormMemoViewModel()
}
ko.applyBindings(window.appViewModel)
流石に再bindingする必要がありそうです…。
カスタムバインディングを作る
ググったら、ヒントがありました。
Stack Overflow: knockout data-bind on dynamically generated elements
上記URLの一番下に書かれていたバインディングを作ればよさそうです。
要は、htmlを反映したタイミングでko.applyBindingsToDescendants
を実行して現在のBindingContextに追加する感じなのですが、それをobservableに反映したタイミングで行うものです。
CoffeeScriptで書き直したのが以下になります。
ko.bindingHandlers.htmlWithBinding = {
init: ->
controlsDescendantBindings: true
update: (element, valueAccessor, allBindings, viewModel, bindingContext) ->
element.innerHTML = ko.utils.unwrapObservable(valueAccessor())
ko.applyBindingsToDescendants(bindingContext, element)
}
使ってみる
htmlにはAjaxで取得したhtmlを入れるobservable(formHtml)をカスタムバインディング付きで定義します。
<div id="form-memo" data-binding="with: formMemoViewModel">
<div data-bind="htmlWithBinding: formHtml"></div>
</div>
formを取得する処理では、取得したhtmlをobservableに入れます。HTMLに直では入れません。
$.get Routes.new_memo_path(), (data) ->
window.app_view_model.formMemoViewModel.formHtml(data)
class @FormMemoViewModel
constructor: ->
@formHtml = ko.observable()
# なんらかの処理
$ ->
window.appViewModel = {
formMemoViewModel: new FormMemoViewModel()
}
ko.applyBindings(window.appViewModel)
このようにしたところ、Ajaxで取得したhtmlに対してもKnockout.jsのbindが有効になりました。よかったよかった。