Angularディレクティブを書くとき、DOMの振る舞い、内容、およびディレクティブが宣言されている要素の外観を操作するために以下の関数のいずれかを使うことができます。
どちらの関数を使うべきかについてはいくらか混乱があるようです。この質問はカバーしています:
次の plunk に基づいて、次のHTMLマークアップを検討してください。
<body>
<div log='some-div'></div>
</body>
次の指令宣言を使用します。
myApp.directive('log', function() {
return {
controller: function( $scope, $element, $attrs, $transclude ) {
console.log( $attrs.log + ' (controller)' );
},
compile: function compile( tElement, tAttributes ) {
console.log( tAttributes.log + ' (compile)' );
return {
pre: function preLink( scope, element, attributes ) {
console.log( attributes.log + ' (pre-link)' );
},
post: function postLink( scope, element, attributes ) {
console.log( attributes.log + ' (post-link)' );
}
};
}
};
});
コンソール出力は次のようになります。
some-div (compile)
some-div (controller)
some-div (pre-link)
some-div (post-link)
compile
が最初に実行され、次にcontroller
、次にpre-link
、最後がpost-link
の順に実行されていることがわかります。
注: 以下は、リンク関数内で子をレンダリングする指令には適用されません。かなりの数のAngularディレクティブがそれを行います(ngIf、ngRepeat、または
transclude
を含むディレクティブなど)。これらのディレクティブは、beforeという名前のlink
関数をネイティブに持ちます。それらの子ディレクティブcompile
が呼び出されます。
オリジナルのHTMLマークアップは、ネストされた要素で構成され、それぞれ独自のディレクティブを持っています。次のマークアップのようになります( plunk を参照)。
<body>
<div log='parent'>
<div log='..first-child'></div>
<div log='..second-child'></div>
</div>
</body>
コンソール出力は次のようになります。
// The compile phase
parent (compile)
..first-child (compile)
..second-child (compile)
// The link phase
parent (controller)
parent (pre-link)
..first-child (controller)
..first-child (pre-link)
..first-child (post-link)
..second-child (controller)
..second-child (pre-link)
..second-child (post-link)
parent (post-link)
ここでは、2つのフェーズ(compileフェーズとlinkフェーズ)を区別できます。
DOMがロードされるとAngularはコンパイル段階を開始し、そこでマークアップを上から下にたどり、すべてのディレクティブでcompile
を呼び出します。グラフィカルに表現すると、次のようになります。
この段階では、コンパイル関数が取得するテンプレートはソーステンプレートです(インスタンステンプレートではありません)。
DOMインスタンスは多くの場合、単純にDOMにレンダリングされたソーステンプレートの結果ですが、ng-repeat
によって作成されるか、またはその場で導入されることがあります。
ディレクティブを持つ要素の新しいインスタンスがDOMにレンダリングされるたびに、リンクフェーズが始まります。
このフェーズでは、Angularがcontroller
、pre-link
を呼び出し、子を繰り返し、すべてのディレクティブでpost-link
を呼び出します。
さまざまなディレクティブ関数は、$compile
(ディレクティブのcompile
が実行される場所)および内部関数nodeLinkFn
(ディレクティブのcontroller
、preLink
、およびpostLink
が実行される場所)という2つの角度関数の中から実行されます。ディレクティブ関数が呼び出される前後で、角度関数内ではさまざまなことが起こります。おそらく最も顕著なのは、子の再帰です。次の簡略図は、コンパイルフェーズとリンクフェーズの主な手順を示しています。
これらの手順を説明するために、次のHTMLマークアップを使用しましょう。
<div ng-repeat="i in [0,1,2]">
<my-element>
<div>Inner content</div>
</my-element>
</div>
次のディレクティブを使って:
myApp.directive( 'myElement', function() {
return {
restrict: 'EA',
transclude: true,
template: '<div>{{label}}<div ng-transclude></div></div>'
}
});
compile
APIは以下のようになります。
compile: function compile( tElement, tAttributes ) { ... }
提供される要素と属性がインスタンスのものではなくソーステンプレートのものであることを示すために、パラメータの前にt
が付いていることがよくあります。
compile
への呼び出しの前に、トランスクルーズされたコンテンツ(もしあれば)が削除され、テンプレートがマークアップに適用されます。したがって、compile
関数に提供される要素は、次のようになります。
<my-element>
<div>
"{{label}}"
<div ng-transclude></div>
</div>
</my-element>
この時点では、変換されたコンテンツは再挿入されません。
ディレクティブの.compile
への呼び出しに続いて、Angularは、ディレクティブによって導入されたばかりのもの(テンプレート要素など)を含む、すべての子要素をトラバースします。
この例では、上記のソーステンプレートの3つのインスタンスが(ng-repeat
によって)作成されます。したがって、次のシーケンスはインスタンスごとに1回、3回実行されます。
controller
APIには以下が含まれます。
controller: function( $scope, $element, $attrs, $transclude ) { ... }
リンクフェーズに入ると、$compile
を介して返されたリンク関数にスコープが提供されます。
最初に、リンク関数は要求されれば子スコープ(scope: true
)または独立スコープ(scope: {...}
)を作成します。
その後、instance要素のスコープを指定して、コントローラが実行されます。
pre-link
APIは以下のようになります。
function preLink( scope, element, attributes, controller ) { ... }
ディレクティブの.controller
の呼び出しと.preLink
関数の呼び出しの間に事実上何も起こりません。 Angularは、それぞれの使用方法に関する推奨事項を引き続き提供します。
.preLink
呼び出しに続いて、link関数は各子要素をトラバースします - 正しいlink関数を呼び出し、それに現在のスコープ(子要素の親スコープとして機能する)をそれにアタッチします。
post-link
APIはpre-link
関数のものと似ています。
function postLink( scope, element, attributes, controller ) { ... }
おそらく、ディレクティブの.postLink
関数が呼び出されると、すべての子の.postLink
関数を含め、そのすべての子要素のリンク処理が完了したことに気付く価値があります。
これは、.postLink
が呼び出されるまでに、子供たちは「生きている」準備ができていることを意味します。これも:
この段階のテンプレートは、このようになります。
<my-element>
<div class="ng-binding">
"{{label}}"
<div ng-transclude>
<div class="ng-scope">Inner content</div>
</div>
</div>
</my-element>
4つの関数すべてを使用する場合、ディレクティブはこの形式に従います。
myApp.directive( 'myDirective', function () {
return {
restrict: 'EA',
controller: function( $scope, $element, $attrs, $transclude ) {
// Controller code goes here.
},
compile: function compile( tElement, tAttributes, transcludeFn ) {
// Compile code goes here.
return {
pre: function preLink( scope, element, attributes, controller, transcludeFn ) {
// Pre-link code goes here
},
post: function postLink( scope, element, attributes, controller, transcludeFn ) {
// Post-link code goes here
}
};
}
};
});
Compileがリンク前とリンク後の両方の関数を含むオブジェクトを返すことに注意してください。 Angular lingoでは、コンパイル関数は テンプレート関数 を返します。
pre-link
が必要でなければ、コンパイル関数は定義オブジェクトの代わりにリンク後の関数を単に返すことができます。
myApp.directive( 'myDirective', function () {
return {
restrict: 'EA',
controller: function( $scope, $element, $attrs, $transclude ) {
// Controller code goes here.
},
compile: function compile( tElement, tAttributes, transcludeFn ) {
// Compile code goes here.
return function postLink( scope, element, attributes, controller, transcludeFn ) {
// Post-link code goes here
};
}
};
});
時々、(post)compile
メソッドが定義された後に、link
メソッドを追加したいことがあります。これには、次のものを使用できます。
myApp.directive( 'myDirective', function () {
return {
restrict: 'EA',
controller: function( $scope, $element, $attrs, $transclude ) {
// Controller code goes here.
},
compile: function compile( tElement, tAttributes, transcludeFn ) {
// Compile code goes here.
return this.link;
},
link: function( scope, element, attributes, controller, transcludeFn ) {
// Post-link code goes here
}
};
});
コンパイル関数が必要ない場合は、その宣言を完全にスキップして、ディレクティブの設定オブジェクトのlink
プロパティの下にリンク後の関数を指定できます。
myApp.directive( 'myDirective', function () {
return {
restrict: 'EA',
controller: function( $scope, $element, $attrs, $transclude ) {
// Controller code goes here.
},
link: function postLink( scope, element, attributes, controller, transcludeFn ) {
// Post-link code goes here
},
};
});
上記のどの例でも、必要でなければ単にcontroller
関数を削除することができます。例えば、post-link
関数だけが必要な場合は、次のものを使用できます。
myApp.directive( 'myDirective', function () {
return {
restrict: 'EA',
link: function postLink( scope, element, attributes, controller, transcludeFn ) {
// Post-link code goes here
},
};
});
AngularがDOM操作を許可するという事実は、コンパイルプロセスへの入力マークアップが出力と異なることがあることを意味します。特に、いくつかの入力マークアップはDOMにレンダリングされる前に数回クローンされるかもしれません(ng-repeat
のように)。
Angularの用語は少し矛盾していますが、それでも2種類のマークアップを区別します。
次のマークアップはこれを示しています。
<div ng-repeat="i in [0,1,2]">
<my-directive>{{i}}</my-directive>
</div>
ソースHTMLで定義されている
<my-directive>{{i}}</my-directive>
これはソーステンプレートとして機能します。
しかし、それがng-repeat
ディレクティブの中にラップされているので、このソーステンプレートはクローンされます(我々の場合では3回)。これらのクローンはインスタンステンプレートであり、それぞれがDOMに表示され、関連する範囲にバインドされます。
各ディレクティブのcompile
関数は、Angularブートストラップのときに一度だけ呼び出されます。
公式には、これはスコープやデータバインディングを含まない(ソース)テンプレート操作を実行する場所です。
主に、これは最適化の目的で行われます。次のマークアップを検討してください。
<tr ng-repeat="raw in raws">
<my-raw></my-raw>
</tr>
<my-raw>
ディレクティブはDOMマークアップの特定のセットをレンダリングします。だから我々はどちらかできます。
ng-repeat
にソーステンプレート(<my-raw>
)の複製を許可してから、各インスタンステンプレートのマークアップを(compile
関数の外側で)変更します。compile
関数内の)目的のマークアップを含むようにソーステンプレートを変更し、ng-repeat
でそれを複製できるようにします。raws
コレクションに1000個のアイテムがある場合、後者のオプションは前者のものより速いかもしれません。
各ディレクティブのcontroller
関数は、新しい関連要素がインスタンス化されるたびに呼び出されます。
公式には、controller
関数は次のとおりです。
繰り返しますが、ディレクティブに独立スコープが含まれている場合、その親スコープから継承されたその中のプロパティはまだ使用できないことを覚えておくことが重要です。
post-link
関数が呼び出されると、バインド、除外など、これまでのすべてのステップが実行されています。
これは通常、レンダリングされたDOMをさらに操作するための場所です。
各ディレクティブのpre-link
関数は、新しい関連要素がインスタンス化されるたびに呼び出されます。
コンパイル順のセクションで前述したように、pre-link
関数はparent-then-childと呼ばれ、post-link
関数はchild-then-parent
と呼ばれます。
pre-link
関数はめったに使われませんが、特別なシナリオで役に立つことができます。たとえば、子コントローラが自分自身を親コントローラに登録したとしても、登録はparent-then-child
方式で行われる必要があります(ngModelController
はこのようにして処理を行います)。