Vue.js 2.0でtwoWayなカスタムディレクティブを実装する方法
以下はvuejs-jp slackに投稿したネタですが、ここのブログにも書いておきます。
先日正式リリースされたVue.js 2.0では、React.jsと同様のVirtual DOMやOne way data flowが採用され高速描画が可能となった反面、ディレクティブは大幅に機能縮小され、"twoWay: true"による双方向データ同期機能は無くなってしまいました。
migration方法としては「コンポーネントで書き直してください」が公式回答なわけですが、正直言うとその辺の辛さがあってReactではなくVueを使っていた人間としては「うーむ」という感想なのです。ダッタラ初めからReactで良くね?…
というわけでいろいろ試行錯誤したところ、実はちょっとしたテクニックで2.0でも依然としてtwoWayなカスタムディレクティブを実装可能なことがわかりました。
ポイントはv⇒vm方向のデータ同期に使っていた
this.set(val);
を
Vue.set(vnode.context, binding.expression, val);
に置き換える、になります。
これだけでは分かる人にしか分からないので、以前に作成した3-stateチェックボックスの例で具体的に説明します。
1.0/2.0それぞれのLiveデモは以下
まず前提知識として、2.0ではディレクティブがインスタンスを持たなくなりました。これはすなわち1.0での"this.el"や"this.set"等が軒並み使えなくなったことを意味します。
これと"twoWay"プロパティの廃止をもって「双方向なカスタムディレクティブは作れなくなった」というわけです。
一方、one wayすなわちvm⇒v方向のデータ同期は従来と同じupdateフック関数で対応できますが、引数が1.0とは大きく異なります。
2.0のフック関数には、bind, unbind, update他の全関数共通で引数として"el", "binding", "vnode"が渡されます。それぞれ、
el: 1.0における"this.el"に対応
binding: ディレクティブの各種引数
binding.value: ディレクティブにbindされたvmオブジェクトの現在値
vnode: ディレクティブに対応するVirtual DOMのnode
という意味なので、update関数は
- update: function(val) { - threeStateByVal(this.el, val); + update: function(el, binding) { + threeStateByVal(el, binding.value); },
という修正で事足ります。
後はv⇒vm方向のデータ同期で使っていた"this.set"の代替ですが、2.0の各種ドキュメントをいろいろ漁っていたところ
vm.$setdeprecated, use Vue.set
という記述を発見しました。これはもしかして、と思ってVue.setの仕様を確認すると、
Vue.set( object, key, value )
Arguments:
{Object} object
{string} key
{any} valueUsage:
Set a property on an object.
となっています。valueは設定したい値そのものなので、あとはobjectとkeyに相当する値、それぞれvmオブジェクトと変数名をどっかから調達できればなんとかなりそうです。
ここで改めて2.0のフック関数に渡される引数を精査したところ、
binding.expression: ディレクティブにbindされたvmオブジェクトの変数名
vnode.context: ディレクティブにbindされたvmオブジェクト
であることがわかりました。ということは、
object = vnode.context
key = binding.expression
value = (v⇒vm方向に同期したい値)
をVue.setに渡せばよい、ということで一番最初に書いたポイントのとおりとなります。
ここまでの内容を整理すると、以下のdiffとなります。
--- 1.0 Sat Oct 22 17:30:36 2016 +++ 2.0 Sat Oct 22 17:31:52 2016 @@ -1,17 +1,17 @@ Vue.directive('three-state', { - twoWay: true, - bind: function() { + //twoWay: true, + bind: function(el, binding, vnode) { this.handler = function() { - var val = threeStateByClick(this.el); - this.set(val); + var val = threeStateByClick(el); + Vue.set(vnode.context, binding.expression, val); }.bind(this); - this.el.addEventListener('click', this.handler); + el.addEventListener('click', this.handler); }, - unbind: function() { - this.el.removeEventListener('click', this.handler); + unbind: function(el) { + el.removeEventListener('click', this.handler); }, - update: function(val) { - threeStateByVal(this.el, val); + update: function(el, binding) { + threeStateByVal(el, binding.value); }, });