状態の初期値とangularのonetime bindingの表示について

はじめに

angularのonetime bindingは一度だけwatch対象にするらしい。一度だけというのはどのような解釈で一度だけかというのが気になった。

結論

ドキュメント に書いてあった。

One-time binding

An expression that starts with :: is considered a one-time expression. One-time expressions will stop recalculating once they are stable, which happens after the first digest if the expression result is a non-undefined value (see value stabilization algorithm below).

実験

初期値だけを変えたdirectiveを作り、少しだけ遅延させ状態を変更させてみる。結果は以下の通り(ng-isolate-scopeとng-bindingは1文字に書換えている)。

----------------------------------------
<div>
<default-undefined @><pre %></pre></default-undefined>
<default-null @><pre %></pre></default-null>
<default-empty @><pre %></pre></default-empty>
<default-string @><pre %>default</pre></default-string>
</div>
----------------------------------------
<div>
<default-undefined @><pre %>updated</pre></default-undefined>
<default-null @><pre %></pre></default-null>
<default-empty @><pre %></pre></default-empty>
<default-string @><pre %>default</pre></default-string>
</div>

状態がundefinedの場合以外にはonetime bindingが初期値の表示で終了してしまう。

gist: https://gist.github.com/podhmo/19ad24485d987585f17c

親のcontrollerの状態を子controllerに渡す場合にはどうするべきか?

親のtitleという状態を子controllerに渡す場合を考えてみる。

1つは上に見たように状態の初期値をundefinedにする方法。 一方で、親の状態が空文字列で初期化された場合にはどうするか考えてみる。

空文字列で初期化した場合

以下の様な形では空文字列でbindingされてしまう。

parent:

<child title="p.title"></child>

child:

<pre>{{ ::c.title() }}</pre> <!-- &でbinding -->

もちろん結果は以下の通り

<parent @><child title="p.title" @><pre %></pre></child></parent>
updated:
<parent @><child title="p.title" @><pre %></pre></child></parent>

初期値の空文字列のままになってしまう。

ng-ifを付けてごまかす場合

gist: https://gist.github.com/podhmo/1d4e998fcabc4439f1cb

1つには以下のようにng-ifを付けてごまかす方法がある。

parent:

<child ng-if="!!p.title" title="p.title"></child>

child:

<pre>{{ ::c.title() }}</pre> <!-- &でbinding -->

結果は以下のとおりになる。

<parent @><!-- ngIf: !!p.title --></parent>
updated:
<parent @><!-- ngIf: !!p.title --><child ng-if="!!p.title" title="p.title" class="ng-scope ng-isolate-scope"><pre %>updated</pre></child><!-- end ngIf: !!p.title --></parent>

ng-ifにもonetime bindingを付けることができるが、そうなると、偽と認識された後にng-ifの評価が働かなくなるのでダメ。

<!-- ダメ -->
<child ng-if=":: !!p.title" title="p.title"></child>

controller自体がpromise等によって初期化が遅延されている場合にはどうすれば良いか?

gist https://gist.github.com/podhmo/edd3dd46d75f936c9680

Object.definePropertyでgetterを設定するというのが良いかもしれない。 例えば、thisの_titletitleというgetterが見るようにする。promiseでは_titleに値を代入する。

{
  restrict: "E",
  scope: {},
  bindTocontroller: {},
  controller: function(){
    Object.defineProperty(this, "title", {get: function(){return this._title;}});

    $timeout(function(){
      this._title = "updated";
    }.bind(this), 20);
  },
  controllerAs: "c",
  template: "<p>{{ ::c.title }}"
}

これならどうにかなる。typescriptなどならpropertyなどの定義はそんなに大変ではない。

<item @><p %></p></item>
update:
<item @><p %>updated</p></item>