LoginSignup
3
4

More than 5 years have passed since last update.

AngularJS 1.4.8 と 1.4.9 で変更された Directiveの挙動

Last updated at Posted at 2016-03-17

AngularJSのバージョンを上げるぞ応援キャンペーン(?)
AngularJS1.4.x でのマイナーバージョンアップで詰まった話しを書くぞよ。

経緯など

AngularJS1.4.8 に、以下の様なコードを書いていた。

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 the attr attribute. If no attr 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 property localModel will reflect the value of parentModel on the parent scope. Any changes to parentModel will be reflected in localModel and any changes in localModel 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' } の時、localModelparentModel の値を反映するようになるでしょう。parentModel で何らかの変更がされると、localModel に変更が反映され、localModel が変更されると、その変更は parentModel に反映されます。もし、親スコーププロパティがない場合、このディレクティブは例外 $compile:nonassign をスローします。この挙動は、=? もしくは =?attr を使うことで、プロパティがオプションとなり、回避することができます。もし、変更をshallow watchによって監視したい場合は (例: $watch の代わりに $watchCollectionを使うような場合) =*=*attr を使うことができます。(プロパティがオプションの場合、=*?=*?attr のように指定ができます。)

ほうほう。つまり、上記の問題が出ているコードを

AngularJS1.4.9上で動作
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 に書いてよ、、と思いましたが納得しました。

結論

  • directive の scope 新規作成するときの挙動をちゃんと理解しよう。今度、&, = まわりの実装で詰まった話しなどもまとめる。
  • ちゃんとドキュメント読んで正しく実装しよう。
3
4
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
3
4