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

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

## 経緯など

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

```js:AngularJS1.4.8上で動作
app.directive('widget', function() {
  return {
    scope: {
      obj: '=' 
    },
    template: '<div>{{obj}}</div>',
    link: function(scope, element, attrs) {
      scope.obj = scope.obj || 'default';
    }
  }
});
```

```html
<widget></widget>
```

scope を新しく作成し、obj から評価されたオブジェクトが渡ってくるようにする。
上では link 中で、`scope.obj` が空だった場合に `scope.obj` の値を書き換え 'default' にするということをやっている。
AngularJS1.4.8 までは、以下の様な表示がされるはずだ。

```
default
```

- 検証コード: https://jsfiddle.net/kawahara/L1x435zv/1/

では、AngularJS1.4.9 の場合はどうだろう。
答えは、エラーで出力されない。
代わりに以下のようなメッセージが表示されるだろう。

```
Error: [$compile:nonassign] Expression 'undefined' used with directive 'widget' is non-assignable!
```

そもそも今回は単純なケースを上げたため、本来 scope なんぞ使わず、
attribute から読み出すなどして使えばよいのだが、通常時はオブジェクトを渡し、場合によっては渡さず
デフォルトを `{}` といったオブジェクトを scope の値に入れて使いたいということがあった。
(そもそも、こういう目的で scope を新規で作るべきではないなど、いろいろあるかもしれないが。。)

何はともあれ、1.4.9 からこういう書き方はできなくなってしまった。

- 検証コード: https://jsfiddle.net/kawahara/0omhm4uv/

## ドキュメントを再び読む

[$compileのドキュメント](https://code.angularjs.org/1.4.9/docs/api/ng/service/$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](https://docs.angularjs.org/error/$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](https://docs.angularjs.org/error/$compile/nonassign) をスローします。この挙動は、`=?` もしくは `=?attr` を使うことで、プロパティがオプションとなり、回避することができます。もし、変更をshallow watchによって監視したい場合は (例: `$watch` の代わりに `$watchCollection`を使うような場合) `=*` か `=*attr` を使うことができます。(プロパティがオプションの場合、`=*?` か `=*?attr` のように指定ができます。)

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


```js: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でこんな変更が? メジャーなバージョンアップで変更されたならまだしも。。 本来通りの挙動でなかったから治ったのだろうか?

## 変更の経緯などを探る

とりあえず、チェンジログを見てみよう。

https://github.com/angular/angular.js/blob/master/CHANGELOG.md#149-implicit-superannuation-2016-01-21

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