woshidan's blog

そんなことよりコードにダイブ。

AngularJSのカスタムディレクティブを作って外部ファイルのテンプレートを表示する

修正版です。
テンプレートの指定について、idでの指定はできないことも無いくらいの方法っぽかったので修正してあげ直しました。ついでにクドいところも若干直しました。
あと、gistが多すぎて読み込みが重かったので、ソースコード部分にgistを使わないようにして、
タイトルも入門っぽいまとめとして書いたことは全部書き終わってからまとめ記事に書こうということで、変更しました。

内容

  • ディレクティブとは
  • モジュールにディレクティブを登録する
  • ディレクティブのテンプレートを指定して表示する
  • ディレクティブのテンプレートを外部ファイルにする
    • テンプレートファイルを相対パスで指定する場合
      • ローカルでテストするために同一生成元ポリシーを一時的に回避する
    • テンプレートを同じhtmlファイルの<script>要素に書いておいて、idで指定する場合
    • $templateCacheオブジェクトを利用する場合

主に参考にしているのは、
https://docs.angularjs.org/guide/directive
http://js.studio-kingdom.com/angularjs/guide/directive

AngularJSアプリケーション開発ガイド

AngularJSアプリケーション開発ガイド

です。

テンプレートの外部ファイルからの読み込みをローカルで試すには少し設定作業が必要で、本記事ではmac OSX, Chromeの場合の手順を説明しています。

ディレクティブとは

AngularJSではDOM要素の並びや命令等を一セットにした関数みたいなもの。

ディレクティブという用語を使うとなんだか難しい気がするので、AngularJSネイティブっぽくオリジナルなAPIを追加する機能がついてるんだーくらいでいいと思います。

少しだけ操作が複雑だけどほとんど同じデータ構造のデータを繰り返し使うみたいなところで使うと楽ちんになったり、テストがしやすくなったりします。

楽ちんにならないんだったら、現状だとむしろ他の人が読めなくなる可能性があるので、素直にng-repeatとか組み込みのディレクティブを組み合わせた方がいいような気がしています。

人が管理しやすくなるための機能で管理しにくくなることは無いです。

モジュールにディレクティブを登録する

まず、.directive()メソッドでモジュールにディレクティブを登録してみましょう。

// sampleApp = angular.module("myModule");とモジュールが定義されているとする。
sampleApp.directive("mySampleDirective", [ '$http', '$scope', function( $http, $scope) {
  var mySampleDirectiveDefinition = {
    // ディレクティブのプロパティを書きます。
  };
  return mySampleDirectiveDefinition;
}]);

ここで注意することは3つです。

  • コントローラのときと同じく、ディレクティブの中で使いたいサービス(たとえば$httpや$filter,$resourceなど)を.directive()メソッドのファクトリー関数の引数にいれておく
  • ディレクティブの名前は小文字から始める
  • このディレクティブを使うとき、ディレクティブを置くhtml上に書くマーカーはmy-sample-directiveになる
    • マーカーについては他にも色々ありますが、それは各自調べてください
  • ディレクティブの定義を書いたオブジェクトを定義して、ファクトリー関数の中で返す

これだけか、という感じですが、これでディレクティブ自体は完成です。

しかし、これだけだとディレクティブのマーカー(my-sample-directive)を使っても何も表示されないので、きちんと定義できているかどうかよく分かりません。

ディレクティブを指定したときに表示されるテンプレートを設定しましょう。

ディレクティブのテンプレートを指定して表示する

sampleApp.directive("mySampleDirective", [ '$http', '$scope', function( $http, $scope) {
  var mySampleDirectiveDefinition = {
    restrict: 'A',
    template: '<span>my sample directive</span>'
  };
 
  return mySampleDirectiveDefinition;
}]);

restrictプロパティでは、テンプレート内でこのディレクティブのマーカー(my-sample-directive)を、属性名(A)、要素名(E)、C(クラス名)やM(コメント)のどこに書くべきか、といった指定するためのプロパティです。先ほどのように、何も指定していない場合は、restrictの値は'A'となっています。

'AE'で属性名に書いても要素名に書いてもいいといった指定も出来ます。

さて、上記のようにAを指定しておくと、テンプレートで

<div my-sample-directive></div>

と書いたとき、

<div my-sample-directive>
  <span>my sample directive</span>
</div>

と変換されます。

ディレクティブのマーカーをつけたdiv要素の子要素としてtemplateに書いた要素が挿入されるのがいやな場合は、

var mySampleDirectiveDefinition = {
    restrict: 'A',
    template: '<span>my sample directive</span>',
    replace: true
  };

とreplaceプロパティの指定をしてください。

先ほどの

<div my-sample-directive></div>

が、今度は、

<span my-sample-directive>my sample directive</span>

に置換されます。replaceプロパティのデフォルト値はfalseです。

追記: replaceオプションはv 2.0において廃止予定のようです。

ディレクティブのテンプレートを外部ファイルにする

Angularのtemplateをtemplate属性で書いておくと、3行くらいまでだったらいいんですけど、それ以上大きくなってくると大変ですよね。

というわけで、さっきの段階では特に大変じゃないと思いますが、練習として外部のファイルに括りだしておきましょう。

外部テンプレートファイルをUrlで指定する場合

以下のようなファイル構成で進めます。

-- angular-app -+- index.html
                +- app.js // sampleAppモジュールの定義してあるファイル
                +- directives - my_sample_directive.js
                +- templates - my_sample_directive.html
// directives/my_sample_directive.js
sampleApp.directive("mySampleDirective", [ '$http', '$scope', function( $http, $scope) {
  var mySampleDirectiveDefinition = {
    restrict: 'A',
    templateUrl: 'templates/my_sample_directive.html'
  };
 
  return mySampleDirectiveDefinition;
}]);
// templates/my_sample_directive.html
<span>my sample directive</span>
// index.html
<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js"></script>
  <script src="app.js"></script>
  <script src="directives/my_sample_directive.js"></script>
</head>
<body ng-app="sampleApp">
  <div id="container" ng-controller="sampleCtrl">
    <div my-sample-test></div>
  </div>
</body>
</html>

index.htmlを開いて表示してみましょう。 Chromeの場合、my sample directiveと表示されていないはずです。

ローカルでテストするために同一生成元ポリシーを一時的に回避する

デベロッパーコンソールを開くと、「同一生成元ポリシーというセキュリティ上の制約のために、指定されたプロトコル以外でファイルを読み込むことができない」という旨のエラーメッセージが表示されています。

そして、file://はその指定されたプロトコルには入っていません。これでは困るので、

  • アプリケーションをWebサーバ経由で読み込む
    • ちょっとだけherokuにあげてみる
  • Chromeの設定を変更する

などの方法を取る必要があります。今回は後者にします。

Chromeの設定について参考にしたのは、http://chrome.half-moon.org/43.htmlです。

まず、Chromeへのパスが通っていなければ、.bash_profileファイルなどを変更してChromeへのパスを通すかエイリアスを作成するかして、 Chromeを簡単にターミナルから起動できるようにしてください。

(mac OSXの場合の手順です。windowsの方は参考先を見てください。)

自分の場合は、chromeというエイリアスを作成しました。

// .bash_profile
alias chrome="open /Applications/Google\ Chrome.app"

ターミナルを再起動して、Chromeを終了してください。
xボタンで閉じるだけでなくてDockのオプションから終了させてChromeインスタンスを一旦けしてください。

ターミナルから以下のようにコマンドを打ち込んでください

chrome --args --allow-file-access-from-files

Chromeが開いたら、URLにchrome://versionを入力してバージョン情報の画面を開いてください。

コマンドラインという項目の実行ファイルのパスの後ろの部分が現在起動しているChromeで有効になっているオプションです。

--allow-file-access-from-files

がオプションに含まれているのを確認したら、そのChromeのウィンドウにindex.htmlのURL(file:// ...)を打ち込んでindex.htmlを開いてください。

今度は、ディレクティブのテンプレートファイルが読み込まれるので、

my sample directive

と表示されているはずです。

templateUrlのurlは、相対URLの場合、ページのファイル(index.html)からの相対パスみたいですね。

なお、確認が終わったら、セキュリティのため、DockからChromeを終了させて、--allow-file-access-from-filesが有効になっているインスタンスを消して、新しく立ち上げるようにしてください。

テンプレートを同じhtmlファイルの<script>要素に書いておいて、idで指定する場合

ぶっちゃけ外部のファイルではないんですが、他に利用しているツールとの兼ね合いで相対パスの指定がややこしいことになっている場合や、ブラウザの設定をするのが億劫な場合、こういう方法もあります。

まず、index.htmlの中に<script>要素を使ってテンプレートを書きます。

// index.html
<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js"></script>
  <script src="app.js"></script>
  <script src="directives/my_sample_directive.js"></script>
</head>
<body ng-app="sampleApp">
  <div id="container" ng-controller="sampleCtrl">
    <div my-sample-test></div>
  </div>
  <script type="text/ng-template" id="my_sample_directive.html">
    <span>my sample directive</span>
  </script>
</body>
</html>

ディレクティブのtemplateUrlには相対パスではなくテンプレートを書いた<script>要素のidを指定します。

// directives/my_sample_directive.js
sampleApp.directive("mySampleDirective", [ '$http', '$scope', function( $http, $scope) {
  var mySampleDirectiveDefinition = {
    restrict: 'A',
    templateUrl: 'my_sample_directive.html'
  };
 
  return mySampleDirectiveDefinition;
}]);

この方法はサーバサイドでページの中にディレクティブを書いたhtmlを組み込むような機能がないと、ページごとにテンプレートをコピーする必要があって、再利用しにくくなります。

しかし、実際各種サーバサイドのフレームワークにむしろそういったrender系の機能がついていないことはないと思います。なので、個人的にはあまり気にしていません。

この方法は、テンプレートの取得にXMLHttpRequestを使っていないので、Webサーバを経由せずに利用できるので気軽に動かしてみることが出来ます。

また、前者では、ページが表示されてからディレクティブが表示されるまでにテンプレートを読み込む時間がかかることがありますが、こちらはページと一緒に表示するので、ページが表示されてからの待ち時間が出ないそうです。

(サーバサイドでHTMLを組むことを想定しているのでそもそものページの表示が速いかは分かりません)

$templateCacheオブジェクトを利用する場合

O'Reilly本に書いてあったので、サンプルコードだけ引用しておきます。

背景とかが気になったら買うか、AngularのO'Reilly本は基本的に公式ドキュメントの和訳に近いところが多いので、公式ドキュメントを検索してください。

ある意味一カ所にテンプレートのHTMLが集まるので分かりやすくはあると思うのですが、結局HTMLをインラインの文字列で書いている気がしてならなかったので試す気が……。

var appModule = angular.module('app', []);

appModule.run(function($templateCache) {
  $templateCache.put('helloTemplateCached.html', '<div>こんにちは</div>');
});

appModule.directive('hello', function(){
  return {
    restrict: 'E',
    templateUrl: 'helloTemplateCached.html',
    replace: true
  };
});

追記:
修正前はChromeの設定で詰んだので、ディレクティブを置く同じhtmlファイル内にテンプレートのコードを用意して、script要素のidをtemplateUrlに指定してテンプレートを取得する方法だけを書いてましたが、もう一回挑戦してみたら出来たので両方きちんと載せました。