この記事の例では、AngularJS 命令の使用法について説明します。参考のために共有します。詳細は次のとおりです:
ディレクティブは、AngularJS アプリケーションの最も重要なコンポーネントです。 AngularJS にはすでに多くのディレクティブが付属していますが、特別なディレクティブを自分で作成する必要があることがよくあります。この記事では、カスタム ディレクティブについて説明し、実際の Angular プロジェクトでカスタム ディレクティブを使用する方法を説明します。記事の最後では、Angular 命令を使用して簡単なメモを取るアプリケーションを作成します。
概要
ディレクティブは、新しい構文を導入するものです。ディレクティブは、DOM 要素上に作成され、いくつかの特定の動作が付加されたマークです。たとえば、静的 HTML は日付選択ウィジェットを作成して表示する方法を知りません。この新しい構文を HTML に教えるには、ディレクティブが必要です。このディレクティブは、日付ピッカーとして機能する要素を作成します。このディレクティブを実装する方法については後で説明します。
以前に Angular アプリケーションを作成したことがある場合は、意識したかどうかに関係なく、ディレクティブをすでに使用したことがあります。 ng-model、ng-repeat、ng-show などのディレクティブを使用したことがあるかもしれません。これらのディレクティブはすべて、特定の機能を DOM 要素にバインドします。たとえば、ng-repeat は特定の要素を繰り返しますが、ng-show は要素を条件付きで表示します。ドラッグ可能な要素を作成したい場合は、ディレクティブの作成が必要になる場合があります。この指令の背後にある基本的な考え方は単純です。イベント リスナーを要素にバインドし、DOM を変換することで、HTML をインタラクティブにします。
jQuery の観点からディレクティブを見てみる
jQuery を使用して日付ピッカーを作成する方法を考えてみましょう。まず HTML に通常の入力フィールドを追加し、次に jQuery で $(element).dataPicker() を呼び出して日付ピッカーに変換します。しかし、考えてみてください。デザイナーがこのマークアップを調べたいとき、このフィールドが何のためのものかをすぐに推測できますか?それは単なる通常の入力フィールドですか、それとも日付ピッカーですか?これを確認するには jQuery を調べる必要があります。 Angular のアプローチは、ディレクティブを使用して HTML を拡張することです。したがって、日付ピッカー ディレクティブは次のようになります:
<date-picker></date-picker>
または次のようになります:
<input type='text' data-picker/>
UI コンポーネントを作成するこの方法は直感的で明確です。要素を見てその目的を知ることができます。
カスタム ディレクティブを作成する
Angular ディレクティブは次の 4 つの形式で表示されます。
1. 新しい HTML 要素 (8ea3043659788eba89a22cfe1e88c5f34ece27668a4ba2090c2be7b456e9e8ff)
2. 要素の属性 (d177832ff31141153aacd78dc9230252)
3. クラスとして (0e9f31b62bd356ba216eece7fdfe0bf1)
4. アノテーションとして (76234e9eaf89f59508e30845d1d07172)
もちろん、ディレクティブが HTML 内でどのように表示されるかを完全に決定できます。ここで、典型的な Angular ディレクティブがどのように記述されるかを見てみましょう。これはコントローラーの登録メソッドに似ていますが、構成ディレクティブのいくつかのプロパティを含む単純なオブジェクト (ディレクティブ定義) を返します。以下のコードは、単純な Hello World ディレクティブを示しています。
var app = angular.module('myapp',[]); app.directive('helloWorld',function(){ return { restrict: 'AE', replace: true, template: '<h3>Hello World!</h3>' } });
上記のコードでは、app.diretive() 関数がモジュールに新しいディレクティブを登録します。この関数の最初のパラメータはディレクティブの名前です。 2 番目のパラメーターは、ディレクティブ定義オブジェクトを返す関数です。ディレクティブに $rootScope、$http、$compile などの追加のオブジェクト/サービスへの依存関係がある場合、それらをそこに注入することもできます。このディレクティブは、次に示すように HTML 要素として使用できます:
<hello-world/>
または:
<hello:world/>
または属性として:
<div hello-world></div>
または:
<div hello:world/>
HTML5 と互換性を持たせたい場合は、属性の前に x- または data- プレフィックスを追加できます。したがって、次のタグは helloWorld ディレクティブと一致します:
<div data‐hello‐world></div>
または
<di vx‐hello‐world></div>
注
ディレクティブを一致させる場合、Angular は要素/属性名から接頭辞 x- または data- を削除します。次に、区切り文字 - または : を、登録されたディレクティブと一致するキャメルケース表記に変換します。これが、helloWorld ディレクティブが HTML で使用される場合、実際には hello-world と記述される理由です。
上記の単純なコマンドは静的テキストを表示するだけですが、検討する価値のある興味深い点がまだいくつかあります。このディレクティブ定義オブジェクトでは 3 つのプロパティを使用しました。これら 3 つの属性が何に使用されるかを見てみましょう:
restrict - この属性は、ディレクティブが HTML でどのように使用されるかを指定します (ディレクティブは 4 つの方法で表示されることに注意してください)。この例では、「AE」に設定します。したがって、このディレクティブは HTML 要素または属性として使用できます。ディレクティブをクラスとして使用できるようにするには、restrict を「AEC」に設定します。
template - 这个实行指明了当指令被Angular编译和链接时生成的HTML标记。它不一定是一个简单的字符串。template可以很复杂,其中经常会涉及其它的指令,表达式({{}}),等等。在大多数情况下你可能会想要使用templateUrl而不是template。因此,理想情况下你应该首先将模板放置在一个单独的HTML文件中然后让templateUrl指向它。
replace - 这个属性指明了是否生成的模板会代替绑定指令的元素。在前面的例子中我们在HTML中使用指令为edd05d9b720c954eda8df66606fc7d416d95f50462131a27fde32c4d9df5bdd4,并将replace属性设置为true。因此,在指令编译后,生成的模板代替了edd05d9b720c954eda8df66606fc7d416d95f50462131a27fde32c4d9df5bdd4。最后的输出结果是684271ed9684bde649abda8831d4d355Hello World!39528cedfa926ea0c01e69ef5b2ea9b0。如果你将replace设置为false,默认情况下,输出模板将会被插入到指令被调用的元素中。
link函数和作用域
有一个指令生成的模板是没有用的除非它在正确的作用域中北编译。默认情况下一个指令并不会得到一个新的子作用域。然而,它可以得到父作用域。这意味着如果一个指令位于在一个控制器中那么它将使用控制器的作用域。
为了利用作用域,我们可以使用一个叫做link的函数。它可以通过指令定义对象中的link属性来配置。我们现在对helloworld指令做一些修改一遍当用户在一个input字段中输入一个颜色名称时,Hello Wolld文字的背景颜色会自动发生改变。同样,当一个用户点击Hello World文字时,背景颜色会重置为白色。相应的HTML标记如下所示:
<body ng-controller='MainCtrl'> <input type='text' ng-model='color' placeholder='Enter a color' / > <hello-wolrd/> </body>
修改后的helloWorld指令代码如下所示:
app.directive('helloWorld',function(){ return { restrict: 'AE', replace: true, template: '<p style="background-color:{{color}}"></p>', link: function(scope,elem,attr){ elem.bind('click',function(){ elem.css('background-color','white'); scope.$apply(function(){ scope.color = "white"; }); }); elem.bind('mouseover',function(){ elem.css('cursor','pointer'); }); } } });
注意到link函数被用在了指令中。它接收三个参数:
scope - 它代表指令被使用的作用域。在上面的例子中它等同于符控制器的作用域。
elem - 它代表绑定指令的元素的jQlite(jQuery的一个自己)包裹元素。如果你在AngularJS被包含之前就包括了jQuery,那么它将变成jQuery包裹元素。由于该元素已经被jQuery/jQlite包裹,我们没有必要将它包含在$()中来进行DOM操作。
attars - 它代表绑定指令的元素上的属性。例如,如果你在HTML元素上有一些指令形式为:5525e9281c68be71aaa8c5ced5bd1dc56d95f50462131a27fde32c4d9df5bdd4,你可以在link函数内用attrs.someAttribute来引用这些属性。
link函数主要是用来对DOM元素绑定事件监听器,监视模型属性变化,并更新DOM。在前面的指令代码中,我们绑定了两个监听器,click和mouseover。click处理函数重置了
的背景颜色,而mouseover处理函数则将游标改变为pointer。模板中拥有表达式{{color}},它将随着父作用域中的模型color的变化而变化,从而改变了Hello World的背景色。
Compile函数
Compile函数主要用来在link函数运行之前进行一些DOM转化。它接收下面几个参数:
tElement - 指令绑定的元素
attrs - 元素上声明的属性
这里要注意compile不能够访问scope,而且必须返回一个link函数。但是,如果没有compile函数以依然可以配置link函数。compile函数可以被写成下面的样子:
app.directive('test',function(){ return { compile: function(tElem,attrs){ //在这里原则性的做一些DOM转换 return function(scope,elem,attrs){ //这里编写link函数 } } } });
大多数时候,你仅仅只需要编写link函数。这是因为大部分指令都只关心与注册事件监听器,监视器,更新DOM等等,它们在link函数中即可完成。像是ng-repeat这样的指令,需要多次克隆并重复DOM元素,就需要在link函数运行之前使用compile函数。你可能会问威慑呢么要将两个函数分别使用。为什么我们不能只编写一个函数?为了回答这个问题我们需要理解Angular是如何编译指令的!
指令是如何被编译的
当应用在启动时,Angular开始使用$compile服务解析DOM。这项服务会在标记中寻找指令然后将它们各自匹配到注册的适龄。一旦所有的指令都已经被识别完成,Angular就开始执行它们的compile函数。正如前面所提到的,compile函数返回一个link函数,该函数会被添加到稍后执行的link函数队列中。这叫做编译阶段(compile phase)。注意到即使同一个指令有几个实例存在,compile函数也只会运行一次。
在编译阶段之后就到了链接阶段(link phase),这时link函数就一个接一个的执行。在这个阶段中模板被生成,指令被运用到正确的作用域,DOM元素上开始有了事件监听器。不像是compile函数,lin函数会对每个指令的实例都执行一次。
改变指令的作用域
默认情况下指令应该访问父作用域。但是我们并不像对所有情况一概而论。如果我们对指令暴露了父控制器的scope,那么指令就可以自由的修改scope属性。在一些情况下你的指令可能想要添加一些只有内部可以使用的属性和函数。如果我们都在父作用域中完成,可能会污染了父作用域。因此,我们有两种选择:
一个子作用域 - 这个作用域会原型继承父作用域。
一个隔离的作用域 - 一个全新的、不继承、独立存在的作用域。
作用域可以由指令定义对象中的scope属性定义。下面的例子展示了这一点:
app.directive('helloWorld',function(){ return { scope: true, //使用一个继承父作用域的自作用域 restrict: 'AE', replace: true, template: '<h3>Hello World!</h3>' } });
上面的代码要求Angular为指令提供一个能够原型继承父作用域的子组用于。另一种情形,一个隔离作用域,代码如下所示:
app.directive('helloWorld',function(){ return { scope: {}, //使用一个全新的隔离作用域 restrict: 'AE', replace: true, template: '<h3>Hello World!</h3>' } });
上面的指令使用一个不继承父作用域的全新隔离作用域。当你想要创建一个可重用的组件时隔离作用域是一个很好的选择。通过隔离作用域我们确保指令是自包含的兵可以轻松地插入到任何HTML app中。这种做法防止了父作用域被污染,由于它不可访问父作用域。在我们修改后的helloWorld指令中如果你将scope设置为{},那么代码就不会再正常运行。它将创建一个隔离的作用域然后表达式{{color}}将无法引用隔离作用域中的属性因此值变为undefined。
隔离作用域并不意味着你一点都不能获取到父作用域中的属性。有一些技巧可以使你访问父作用域中的属性同时监听这些属性的变化。我们将在后续文章中提到这种高级技巧。