本文實例講述了AngularJS指令用法。分享給大家參考,具體如下:
指令(directives)是任何AngularJS應用中最重要的成分。儘管AngularJS已經自帶了許多指令,你經常會發現需要自己親手創建一些特別的指令。本文將會帶你了解自訂指令並解釋如何在現實世界中的Angular專案中使用它們。文章的最後,我們將一起用Angular指令創建一個簡單的筆記小應用程式。
綜述
一個指令就是一個引入新語法的東西。指令是在DOM元素上做的標記,並同時附加了一些特定的行為。例如,靜態的HTML並不知道如何來建立並顯示日期選擇插件。為了將這個新文法教給HTML我們需要一條指令。這個指令將會建立一個充當日期選擇器的元素。我們將在隨後看到如何實現這個指令。
如果你之前已經寫過Angular應用,那麼你已經使用過指令了,不管你有沒有意識到這一點。你可能已經使用過像是ng-model,ng-repeat,ng-show等等這樣的指令。所有這些指令都將特定的功能綁定到了DOM元素之上。例如,ng-repeat會重複特定的元素,而ng-show會有條件的展示元素。如果你想要建立一個可拖曳元素的話你可能需要建立一個指令。指令背後的基本想法很簡單。它透過在元素上綁定事件監聽器並且將DOM變形來使HTML變得具有互動性。
從jQuery的角度來看指令
想想你如何使用jQuery來建立一個日期選擇器。我們首先在HTML中新增一個普通的input欄位然後在jQuery中我們呼叫$(element).dataPicker()來將其轉換為一個日期選擇器。但是,考慮一下。當一個設計師想要來檢查這個標記時,他/她能夠立刻猜出這個字段究竟是做什麼用的嗎?它只是一個普通的input字段還是一個日期選擇器?你必須要查看jQuery來確認這一點。 Angular的方法是使用指令來擴充HTML。因此,一個日期選擇器的指示看起來可能如下所示:
<date-picker></date-picker>
或如下所示:
<input type='text' data-picker/>
你可以看到元素就知道它的用途。
建立自訂指令
一個Angular指令可能以四種形式出現:
1.一個新的HTML元素(<date-picker></date-picker>)
2.一個元素上的屬性( )
3.作為一個類別()
4.作為註解()
當然,我們完全可以決定我們的指令以什麼形式出現在HTML中。現在,讓我們來看看一個典型的Angular指令是如何寫成的。它和controller的註冊方式類似,但是它會傳回一個簡單的物件(指令定義),其中那個包含有一些配置指令的屬性。下面的程式碼展示了一個簡單和Hello World指令:
var app = angular.module('myapp',[]); app.directive('helloWorld',function(){ return { restrict: 'AE', replace: true, template: '<h3>Hello World!</h3>' } });
在上面的程式碼中,app.diretive()函數在我們的模組中註冊了一個新的指令。這個函數的第一個參數是指令的名稱。第二個參數是一個傳回指令定義物件的函數。如果你的指令對額外的物件/服務(services)例如 $rootScope, $http 或 $compile 有依賴,它們也可以在其中被注入。這個指令可以當作一個HTML元素來使用,如下所示:
<hello-world/>
或:
<hello:world/>
<div hello-world></div>
如果你想要相容HTML5,你可以在屬性前面加上x-或data-前綴。因此,以下的標記將會符合helloWorld指令:
<div hello:world/>
或
<div data‐hello‐world></div>
或將然後將分隔符號 - 或 : 轉換為駝峰表示法已符合註冊的指令。這就是為什麼我們的helloWorld指令用在HTML中的時候實際上寫成了hello-world。
儘管上面的這個簡單的指令僅僅只是展示了一些靜態的文本,其中還是有一些值得我們去探究的有趣的點。我們已經在這個指令定義物件中使用了三個屬性。讓我們來看看這三個屬性分別都有什麼用:
restrict - 這個屬性指明了一個指令應該如何在HTML中使用(記住指令可以以四種方式出現)。在這個例子中我們將它設定為'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。
隔离作用域并不意味着你一点都不能获取到父作用域中的属性。有一些技巧可以使你访问父作用域中的属性同时监听这些属性的变化。我们将在后续文章中提到这种高级技巧。