AngularJSのバージョンを上げるぞ応援キャンペーン(?)
AngularJS1.4.x でのマイナーバージョンアップで詰まった話しを書くぞよ。
経緯など
AngularJS1.4.8 に、以下の様なコードを書いていた。
app.directive('widget', function() {
return {
scope: {
obj: '='
},
template: '<div>{{obj}}</div>',
link: function(scope, element, attrs) {
scope.obj = scope.obj || 'default';
}
}
});
<widget></widget>
scope を新しく作成し、obj から評価されたオブジェクトが渡ってくるようにする。
上では link 中で、scope.obj
が空だった場合に scope.obj
の値を書き換え 'default' にするということをやっている。
AngularJS1.4.8 までは、以下の様な表示がされるはずだ。
default
では、AngularJS1.4.9 の場合はどうだろう。
答えは、エラーで出力されない。
代わりに以下のようなメッセージが表示されるだろう。
Error: [$compile:nonassign] Expression 'undefined' used with directive 'widget' is non-assignable!
そもそも今回は単純なケースを上げたため、本来 scope なんぞ使わず、
attribute から読み出すなどして使えばよいのだが、通常時はオブジェクトを渡し、場合によっては渡さず
デフォルトを {}
といったオブジェクトを scope の値に入れて使いたいということがあった。
(そもそも、こういう目的で scope を新規で作るべきではないなど、いろいろあるかもしれないが。。)
何はともあれ、1.4.9 からこういう書き方はできなくなってしまった。
ドキュメントを再び読む
$compileのドキュメントによると以下のことが書いてある。
=
or=attr
- set up bi-directional binding between a local scope property and the parent scope property of name defined via the value of theattr
attribute. If noattr
name is specified then the attribute name is assumed to be the same as the local name. Given<widget my-attr="parentModel">
and widget definition of scope:{ localModel:'=myAttr' }
, then widget scope propertylocalModel
will reflect the value of parentModel on the parent scope. Any changes toparentModel
will be reflected in localModel and any changes inlocalModel
will reflect in parentModel. If the parent scope property doesn't exist, it will throw a $compile:nonassign exception. You can avoid this behavior using =? or =?attr in order to flag the property as optional. If you want to shallow watch for changes (i.e.$watchCollection
instead of$watch
) you can use=*
or=*attr
(=*?
or=*?attr
if the property is optional).
適当な訳:
= もしくは =attr は、ローカルスコーププロパティと親スコーププロパティの間の双方向バインディングを属性値
attr
を通して実現するものです。もし、属性attr
を指定しない場合は、ローカル名と同じものになります。<widget my-attr="parentModel">
と、widget の scope 定義が{ localModel:'=myAttr' }
の時、localModel
はparentModel
の値を反映するようになるでしょう。parentModel
で何らかの変更がされると、localModel
に変更が反映され、localModel
が変更されると、その変更はparentModel
に反映されます。もし、親スコーププロパティがない場合、このディレクティブは例外 $compile:nonassign をスローします。この挙動は、=?
もしくは=?attr
を使うことで、プロパティがオプションとなり、回避することができます。もし、変更をshallow watchによって監視したい場合は (例:$watch
の代わりに$watchCollection
を使うような場合)=*
か=*attr
を使うことができます。(プロパティがオプションの場合、=*?
か=*?attr
のように指定ができます。)
ほうほう。つまり、上記の問題が出ているコードを
app.directive('widget', function() {
return {
scope: {
obj: '=?'
},
template: '<div>{{obj}}</div>',
link: function(scope, element, attrs) {
scope.obj = scope.obj || 'default';
}
}
});
のようにすることで、AngularJS1.4.8 と同じ動きをすることができた。
また、AngularJS1.4.8 上で obj: '='
, <widget obj=""></widget>
のような書き方にすることによって、$compile:nonassign
を発生させることができたことを確認した。
というわけで、Directive の双方向バインディングをちゃんと理解せずに使っていたというのと、=?
という書き方を知らなかったというのがこの点の敗因であります。が、なぜ 1.4.9でこんな変更が? メジャーなバージョンアップで変更されたならまだしも。。 本来通りの挙動でなかったから治ったのだろうか?
変更の経緯などを探る
とりあえず、チェンジログを見てみよう。
が、$compile
に関して、これといった記述はない。。なんでや。
では、コードへとダイブしてみましょう。
そしたら、$parse
の挙動が変更されていることに気づく。
https://github.com/angular/angular.js/commit/7bb2414bf6461aa45a983fd322ae875f81814cc4
https://github.com/angular/angular.js/issues/13367
どうやら、Angular1.4になったときに作りこまれたバグのようでした。見事に Angular1.3.9 では Angular1.4.9 と同じような本来の挙動でした。 (属性を渡さなかった時、directive 内で scope 更新した時にエラーとなる。)
それが、1.4.9 になって修正されたということ。むー。CHANGELOG に書いてよ、、と思いましたが納得しました。
- 1.3.9検証コード https://jsfiddle.net/kawahara/t4ox0bfe/
結論
- directive の scope 新規作成するときの挙動をちゃんと理解しよう。今度、
&
,=
まわりの実装で詰まった話しなどもまとめる。 - ちゃんとドキュメント読んで正しく実装しよう。