という奇怪な出来事に遭遇したのでその原因と対策をメモ。
`angular ui-view dupulicate`とか調べてる人には一見の価値ありかもしれません。
きっかけ
- Angularのui-routerを使ってるページで
- 特定のstateに遷移すると
- ui-view="foo"を指定してる要素が増える
DOMのイメージとしては、
<div ui-view="foo"></div> <div ui-view="foo"></div> <div ui-view="foo"></div> <div ui-view="foo"></div> <div ui-view="foo"></div>
みたいになる。
本来の正常な動作であれば、要素は1つだけ残ってその中身だけが置き換わるはず。
原因
というか、調べてみてわかった仕組みからの推察。
DOMの置換まわりがおかしいのは明確なので、どういうロジックでDOMを触ってるかを調べる。
ui-viewディレクティブ
https://github.com/angular-ui/ui-router/blob/legacy/src/viewDirective.js
ディレクティブを処理してるコードがここ。
L:203あたりで、stateが変わるタイミングになんやかんやしてるのがわかる。
肝心のDOMにさわってるのはL:243あたり。
説明するためにコードをそのまま貼って、ちょっと並び替える。
ここで、`previousEl`とか`currentEl`とかなってるのが今回の獲物。
function cleanupLastView() { var _previousEl = previousEl; var _currentScope = currentScope; if (_currentScope) { _currentScope._willBeDestroyed = true; } if (currentEl) { // rendererってなんぞ renderer.leave(currentEl, function() { cleanOld(); previousEl = null; }); previousEl = currentEl; } else { cleanOld(); previousEl = null; } currentEl = null; currentScope = null; function cleanOld() { if (_previousEl) { _previousEl.remove(); // これで消してるなら消えるはず・・ } if (_currentScope) { _currentScope.$destroy(); } } }
rendererとはなんだ問題
ui-routerのstateが切り替わるときに、アニメーションで遷移させたい!需要があったんでしょうね。
その対応をするために、rendererってのを噛ませて、`**-leave`とか`**-enter`ってクラス名を捌いたり、アニメーションの処理をしてる。
ドキュメントこれ。
Frequently Asked Questions · angular-ui/ui-router Wiki · GitHub
そしてng-animate
今回の罠にハマるのは、おそらくng-animateを読み込んでるプロジェクト。
なんで突然ng-animateの話になるかというと、このrendererの処理がこいつの存在によって変わるから。
L:155あたりが正にそれで、ng-animateがある場合はコールバックがPromise.then()で処理されるようになってる。
このコールバックの処理がさっき見てた古いui-viewなDOMを削除してるとこ。
で、この削除のロジックを通ってるならDOMは消えるはずーってことでブレークポイント張ってみるも、待てども待てども処理されない!
どうやら直前のif文をかいくぐってるっぽい。
というわけで、古いDOMを`remove()`する処理の判定が走るはずのところが、ng-animateの非同期な処理のせいでズレて、古いui-viewな要素が消えず、どんどん増えていく、と。
対策
ようはng-animateがない場合の処理を通せばいいわけなので・・。
// [0] そもそもDIしない angular.module('app', [/* 'ng-animate' */]); // [1] まとめてOFF $animate.enabled(false); // [2] 個別にOFF $animate.enabled(el, false); // el = ui-viewなやつ
もしくは、
<!-- [3] noanimation="true" --> <div ui-view="foo" noanimation="true"></div>
この方式が一番シンプルで良いなーと思ったら、
この`noanimation`方式なんと2016-02-07リリースの、いわゆる最新バージョンの0.2.18で追加されたやつ。
おわりに
Angularガチ勢への道のりは遠いですね。