JavaScript
AngularJS

ng-includeの落とし穴

More than 3 years have passed since last update.

htmlを分割したいと思ったときにng-includeはとても便利です。ただし制約もあります。

動きそうで動かないコード

以下の様なコードがあったとします。これはうまく動きそうで動きません。具体的には初期化されたときはHello worldと表示されますが、テキスト欄を変更しても値が更新されることはありません。

index.html
<div ng-controller="Ctrl">
  Hello {{name}}!
  <div ng-include="'path/to/template.html'"></div>
</div>
template.html
<input ng-model="name">
app.js
angular.module('app', []).controller('Ctrl', function($scope) {
  $scope.name = 'world';
});

原因

更新されない原因はng-includeディレクティブが子スコープを作るからです。初期化されたときはng-includeのスコープにはnameプロパティは存在せず親スコープから参照しますが、テキスト欄の値を更新するとng-includeのスコープの方に反映されます。結果、親スコープのnameプロパティは変更されず表示も変わらない、ということになります。

解決法

サーバーサイドでinclude

サーバーサイドでテンプレートをインクルードする方法はいくらでもあるので略。

ディレクティブを作る

以下のようにテンプレートだけ設定されたディレクティブを作ります。

app.directive('foo', function() {
  return {
    template: '<input ng-model="name">'
  };
});
index.html
<div ng-controller="Ctrl">
  Hello {{name}}!
  <div foo></div>
</div>

テンプレートを読み込むだけのディレクティブを作る

ng-includeのようにテンプレートを動的に切り替える必要がないのであれば、以下の様なディレクティブでテンプレートを読みこめば事足ります。

app.directive('myInclude', function($http, $compile) {
  return function(scope, element, attr) {
    $http.get(attr.myInclude).success(function(response) {
      element.html(response);
      $compile(element.contents())(scope);
    })
  };
});
index.html
<div ng-controller="Ctrl">
  Hello {{name}}!
  <div my-include="path/to/template.html"></div>
</div>

一度ディレクティブを作れば、サーバーサイドでのインクルードと似たような書き方ができるので個人的にはこれが一番取っ付きやすいです。

まとめ

ng-includeでhtmlを分割したとき、分割したテンプレートの中にデータを上書きするようなものを入れるとうまく動きません。ng-includeはデータを表示するだけのテンプレートだけを扱い、もしng-model等々使いたい場合は、上に書いたような解決方法を試してみましょう(そもそも規模がでかい場合はui-routerみたいなものを使ったほうがよさそうですが)。

参考

AngularJS: API: ngInclude