指令(directive)是教会HTML新技能的方法。在DOM编译期间,与HTML相匹配的指令会执行。这样,我们就可以利用指令来注册一些动作,或者执行转换DOM的操作。
Angular内置了一些非常有用、并且可被扩展的指令,这样我们就能把HTML变身为DSL(可声明的领域专用语言)。
指令的命名方式采用驼峰式,如'ngBind'。但在调用时,可采用“蛇行式”,以及特定的分隔符::
, -
, _
。甚至可以在它们前面加上x-
或data-
以便通过HTML格式验证。
这里举几个有效的例子:
ng:bind
, ng-bind
, ng_bind
, x-ng-bind
, data-ng-bind
。
指令可被放置于以下位置:
以下几个例子都是完全相同的调用指令myDir
的方法。(不过大多数情况,我们仅会使用属性式指令)
<span my-dir="exp"></span>
<span class="my-dir: exp;"></span>
<my-dir></my-dir>
<!-- directive: my-dir exp -->
指令可被多种不同的方式调用,但在下面例子的测试代码中,它们是完全相同的。
index.html
<!doctype html>
<html ng-app>
<head>
<script src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-controller="Ctrl1">
Hello <input ng-model='name'> <hr/>
<span ng:bind="name"> <span ng:bind="name"></span> <br/>
<span ng_bind="name"> <span ng_bind="name"></span> <br/>
<span ng-bind="name"> <span ng-bind="name"></span> <br/>
<span data-ng-bind="name"> <span data-ng-bind="name"></span> <br/>
<span x-ng-bind="name"> <span x-ng-bind="name"></span> <br/>
</div>
</body>
</html>
script.js
function Ctrl1($scope) {
$scope.name = 'angular';
}
End to end test
it('should show off bindings', function() {
expect(element('div[ng-controller="Ctrl1"] span[ng-bind]').text()).toBe('angular');
});
你可以在jsfiddle上查看并修改该示例
在编译阶段,编译器使用$interpolate
服务来匹配文本和属性,检查它们是否嵌入了表达式。这些表达式注册为“观察者”,在digest周期中将被更新。
以下是一个interpolation
的例子:
Hello {{username}}!
HTML的编译过程,发生在三处:
首先HTML由标准浏览器api解析为DOM。认识到这一点很重要,因为模板代码必须为可解析的HTML。多数模板系统操作字符串,与它们不同的是,angularjs操作DOM元素。
使用$compile()
方法对DOM进行编译。该方法遍历DOM和寻找可匹配的指令(directive),找到后则把它加到该DOM关联的指令列表中。一旦某DOM所有的指令都被找到,则按照指令中定义的优先级对它们进行排序,并调用它们的@compile()
方法。该编译方法有机会修改DOM结构,并且有责任提供一个link()
函数(将在后面解释)。$compile()
方法返回一组连接(linking)方法,它们是一个集合,由该DOM关联的所有指令返回的link函数组成。
调用在上一步中返回的连接(link)函数,将模板与域对象(scope)连接起来。这将依次调用每一个指令(directvie)的连接函数,允许它们在元素上注册任意的监听器(listener)。结果将得到一个在域对象与DOM之间的实时绑定(live binding)。当域对象(scope)变化了,将立刻反映到DOM上。
var $compile = …; // injected into your code
var scope = …;
var html = '
';// Step 1: parse HTML into DOM element
var template = angular.element(html);
// Step 2: compile the template
var linkFn = $compile(template);
// Step 3: link the compiled template with the scope.
linkFn(scope);
现在你也许会好奇,为什么我们要把编译过程分解为编译和连接两个步骤。为了理解这点,让我们看一下真实世界中的repeater
的例子:
Hello {{user}}, you have these actions:
<ul>
<li ng-repeat="action in user.actions">
{{action.description}}
</li>
</ul>
简单来说,只要当模型类改变时需要更新DOM结构(就像repeater),我们就需要将编译与连接分开。
上面的例子在编译时,编译器访问每一个节点来寻找指令(directive)。{{user}}
是插补指令的一个例子,ngRepeat
是另一种。
但ngRepeat
有一个困境:它需要为user.actions
中的每一个action快速生成一个新的<li>
,这表示它需要持有li
元素的一个干净的copy用来复制,每当有新的actions插入时,li
模板元素需要被快速复制被插入到ul
中。
但是仅仅复制是不够的,它还需要编译li
,所以它的指令(如{{action.descriptions}}
也需要在正确的域(scope)中执行一遍。有一种很天真的处理方式是简单地插入一个li
元素的copy并编译它。但是编译每一个li元素会很慢,因为编译过程需要遍历DOM树并寻找指令还要运行它们。如果我们把编译过程放在一个需要展示100个元素的repeater里,我们很快就会遇到性能问题。
解决方案是把编译过程分解为两步:第一步编译,寻找所有的指令并给它们按优先级排序;第二步连接,将把li
的某实例与scope
的某实例连接起来。
ngRepeat
的运行方式不是把编译过程下放到li
元素中,而是它们分开编译。结果是得到一个连接函数,它包括了li
元素中所有将被附加到某一个指定的li的复制体上的指令。在运行期,ngRepeat会监视表达式,每当有新的元素添加到数组中时,它将复制li元素,为它创建一个新的域对象,并调用li元素上的连接函数。
总结:
在本例中,我们将创建一个显示当前时间的指令。
index.html
<!doctype html>
<html ng-app="time">
<head>
<script src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
<script src="script.js"></script>
</head>
<body>
<div ng-controller="Ctrl2">
Date format: <input ng-model='format'> <hr/>
Current time is: <span my-current-time="format"></span
</div>
</body>
</html>
script.js
function Ctrl2($scope) {
$scope.format = 'M/d/yy h:mm:ss a';
}
angular.module('time', [])
// Register the 'myCurrentTime' directive factory method.
// We inject $timeout and dateFilter service since the factory method is DI.
.directive('myCurrentTime', function($timeout, dateFilter) {
// return the directive link function. (compile function not needed)
return function(scope, element, attrs) {
var format, // date format
deferId; // deferId, so that we can cancel the time updates
// used to update the UI
function updateTime() {
element.text(dateFilter(new Date(), format));
}
// watch the expression, and update the UI on change.
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
// schedule update in one second
function updateLater() {
// save the deferId for canceling
deferId = $timeout(function() {
updateTime(); // update DOM
updateLater(); // schedule another update
}, 1000);
}
// listen on DOM destroy (removal) event, and cancel the next UI update
// to prevent updating time ofter the DOM element was removed.
element.bind('$destroy', function() {
$timeout.cancel(deferId);
});
updateLater(); // kick of the UI update process.
}
});
上例可在jsfiddle上查看效果及修改
这里是一个指令的结构骨架,完整的解释在后面。
var myModule = angular.module(…);
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
priority: 0,
template: '<div></div>',
templateUrl: 'directive.html',
replace: false,
transclude: false,
restrict: 'A',
scope: false,
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
},
link: function postLink(scope, iElement, iAttrs) { ... }
};
return directiveDefinitionObject;
});
在大多数情况下,你不需要如此彻底的控制,所以上面的例子可以简化。所以与本示例不同的地方,将在下面几节中讲解。在本节我们仅仅对该示例感兴趣。
第一步是简化代码,尽管使用默认值。所以上面的代码可以简化为:
var myModule = angular.module(…);
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
compile: function compile(tElement, tAttrs) {
return function postLink(scope, iElement, iAttrs) { ... }
}
};
return directiveDefinitionObject;
});
大多数指令只关系它们自己的实例,而不关心模板的转换,所以可以进一步简化为:
var myModule = angular.module(…);
myModule.directive('directiveName', function factory(injectables) {
return function postLink(scope, iElement, iAttrs) { … }
});
工厂方法的责任是创建指令。它只会在编译器第一次匹配到某指令时,被调用一次。你可以在这里执行任意的初始化操作。该方法被$injector.invoke
调用,所以可被注入(injectable),并遵守injection注解的所有规则。
指令定义对象为编译器提供了一些必要信息。有以下属性:
name - 当前域(scope)对象的名称。默认与注册时使用的名称相同
priority - 优先级。当一个DOM元素上有多个指令时,有时候很有必要指定指令的顺序。优先级(priority)用来在编译函数调用之前来对它们进行排序。数字大的排在前面,数字相同的顺序不确定。
terminal - 是否为终结。如果设为true,则与当前priority相同的指令将在最后一批执行(该批中的各指令也将被执行,因为priority相同的指令运行顺序不确定)
scope - 域对象。如果设为
* `true` - 则会为该指令创建一个新的scope。如果在同一元素上的多个指令同时要求创建新scope,则只会创建一个scope。该规则不适用于模板的root,因为root总是创建一个新scope。
{}
(object hash) - 创建一个新的'孤立'的scope。该scope与普通的scope不同的是,它不会以原型方式从上级scope中继承内容。它对于创建一些可利用的组件很有用,因为它不会让我们意外修改了上级scope中的数据。该'孤立'的scope将上级scope中的属性复制到本地的一个object hash中。这些本地属性可为模板提供数据。本地变量的定义是一个由源到本地属性的hash:
* `@` 或 `@attr` - 将scope的一个本地属性绑定到DOM属性。由于DOM属性都是字符串,所以它的值总是字符串。如果属性名称没有指定,则本地变量名称与属性名称一致。假设给定`<widget my-attr="hello {{name}}">`以及定义:`{localName:'@myAttr'}`,则widget scope的属性`localName`将会反映到`hello, {{name}}`中。反过来也一样,如果`name`变了,则`localName`的值也会变。其中`name`是从上级scope中读取的(而不是在当前的组件中)。
* `=` 或 `=attr` - 在本地scope与上级scope的属性之间设置双向绑定。如果`attr`的值没有设置,则本地与上级scope的属性名相同。假设给定`<widget my-attr="parentModel">`以及定义`{scope: { localModel:'=myAttr'}`,则当`localName`的值变了,`parentModel`会跟着变,反之亦然。
&
或 &attr
- (这一段原文有问题,等修正后再翻译)
controller
- Controller的构造函数。该controller将在pre-linking阶段前被初始化,并且可被多个directives通过名称引用共享(参考 require
属性). 因此我们利用它在不同的directives之间通信或改变行为。controller可注入以下对象:
* `$scope` - 当前元素关联的scope* `$element` - 当前元素* `$attrs` - 当前元素对应的属性对象* `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:`function(cloneLinkingFn)`.(这一句没理解)
require
- 要求将另一个controller传入到当前的linking function中。require
要求传入一个controller的名称。如果找不到,则可以控制是否抛出一个异常。controller名称可有以下前缀:
* `?` - 不抛异常(所以依赖的controller为可选)* `^` - 继续向上级元素中寻找
restrict
- EACM
这四个字母中的一个或多个,默认为'A'。
* `E` - **E**lement name: `<my-directive></my-directive>`
A
- Attribute: <div my-directive="exp"> </div>
C
- Class: <div class="my-directive: exp;"></div>
M
- Comment: <!-- directive: my-directive exp -->
template
- 模板。将使用该模板来替换HTML的内容。该替换过程将把旧元素中的attribute/classes迁移到新元素中(TODO未理解)。参考下面的“创建widget”一节。
templateUrl
- 与template
相同但是从指定的URL导入内容。由于导入是异步,所以compilation/linking过程将被暂停直到模板导入成功。
replace
- 如果设为true
则用前面替换当前元素,否则在后面添加。
(这一段很难,完全没看懂)
transclude
- compile the content of the element and make it available to the directive. Typically used withngTransclude
. The advantage of transclusion is that the linking function receives a transclusion function which is pre-bound to the correct scope. In a typical setup the widget creates an isolate
scope, but the transclusion is not a child, but a sibling of the isolate
scope. This makes it possible for the widget to have private state, and the transclusion to be bound to the parent (pre-isolate
) scope.
* `true` - transclude the content of the directive.* `'element'` - transclude the whole element including any directives defined at lower priority.
compile
- 编译函数,将在下节讲。
link
: 连接函数,将在下节讲。只有当compile属性没有定义时,才会使用本属性。
function compile(tElement, tAttrs, transclude) { … }
编译函数用来转换模板DOM。由于多数directives不需要模板转换,所以它不常用。什么时候才会用到呢?比如像ngRepeat
这样需要转换模板的directive,或者像ngView
这样需要异步导入模板的。
编译函数有以下参数:
tElement
- 模板元素(template element),即directive被声明的那个元素。在各操作中,只有在元素(或子元素)上进行模板转换的操作才是安全的。
tAttrs
- 模板属性(template attributes)。在元素上声明的属性集,它在该元素上所有的directives编译函数中共享。参看Attributes
transclude
- A transclude linking function: function(scope, cloneLinkingFn)
.(未理解)
注意:如果模板被cloned,则template示例与instance实例可能不再相同。所以在编译函数中进行除DOM转换以外的任何操作都是不安全的。特别是,“DOM监听器注册”的操作应该在linking函数中,而不是在compile函数中。
编译函数可以返回一个函数,或者是一个object。
函数: 相当于使用了link
属性且保持compile
为空* object: 如果包含了pre
或post
属性,则可以用来控制linking函数何时被调用。详细内容参考下面内容。
function link(scope, iElement, iAttrs, controller) { … }
Link函数用来注册DOM监听器或更新DOM,当模板被cloned之后被调用。多数时候在这里放置directive的逻辑代码。
scope
- 让directive用它来注册watches
iElement
- instance element - directive使用的元素实例。只有在postLink
中维护当前元素的子元素才是安全的,因为子元素已经被link了。
iAttrs
- instance attributes - 在元素上声明的属性集,它在该元素上所有的directives编译函数中共享。See Attributes
controller
- 一个controller实例 - 如果该元素上至少有一个directive定义了controller。它在不同的directives之间共享,可当作它们互相通信的通道。
在子元素被link之前调用。不要进行DOM转换的操作,因为可能导致编译器的linking函数找不到正确的元素而失败。
在子元素被link之后调用。在这里执行DOM转换操作是安全的。
属性对象可当作link()
和compile()
函数的参数传过去 ,它提供了一种访问方式:
规范化之后的属性名:由于一个directive可使用多种方式来声明(如ng:bind
,x-ng-bind
),所以该对象提供了一种规范的方式来访问
directive内部通信:元素上所有的directive共享同一个atrribute object,所以可用经作为directive的内部通信载体
支持插补:(TODO)
监听插补属性:使用$observe
来监听包含了表达式的属性的变化(如src="{{bar}}"
)。这种方式不但高效,而且是得到其值的唯一方法,因为在linking阶段插补的值还没有被执行,所以这个值在当前还是undefined
.
function linkingFn(scope, elm, attrs, ctrl) {
// get the attribute value
console.log(attrs.ngModel);
// change the attribute
attrs.$set('ngModel', 'new value');
// observe changes to interpolated attribute
attrs.$observe('ngModel', function(value) {
console.log('ngModel has changed value to ' + value);
});
}
我们都想要可复用的组件。下面是一个简化的对话框组件的假想工作方式。
visible="show"
on-cancel="show = false"
on-ok="show = false; doSomething()">
Body goes here: {{username}} is {{title}}.
点击“show"按钮,将打开对话框。该对话框包含一个标题,其数据被绑定到username
上。它还包含一个body,将被我们transclude到dialog中。
下面是为该对话框部件准备的模板代码示例:
这段代码还不能正确的渲染,除非我们来点魔术。
我们需要解决的每一个问题是dialog要有一个title,并且它要与username
绑定。另外在scope中,还需要有onOk
和onCancel
函数。这都将限制该widget的用途。为了解决这些问题,我们将使用locals
来创建一些本地变量,与模板相匹配:
scope: {
title: 'bind', // set up title to accept data-binding
onOk: 'expression', // create a delegate onOk function
onCancel: 'expression', // create a delegate onCancel function
show: 'accessor' // create a getter/setter function for visibility.
}
但在widget的scope上创建本地属性,会产生两个问题:
isolation - 如果用户忘了给title设置,dialog会尝试到上级scope中寻找。这会产生未知的问题。
transclusion - transcluded DOM可以看到widget的本地变量,可能会覆盖transclusion用来数据绑定的属性。在我们的例子中,widget的title
属性就会灭掉transclusion的title属性。(TODO:翻译有问题)
为了解决第一个问题,directive将声明一个新的isolated scope。一个isolated scope不会以原型方式从子类中继承属性,所以我们不用担心意味损坏了数据。
但isolated scope又带来了一个新问题:如果transcluded DOM是该widget的isolated scope的子域,则它不会继承到任何数据。因此,transcluded scope是原scope的子,并在widget创建的isolated scope之前。于是,transcluded与widget两者的isolated scope成了兄弟。
这看起来有点复杂,不过这可以让widget用户和开发者尽量不会觉得意外。
因此,最终的directive的声明看起来如下:
transclude: true,
scope: {
title: 'bind', // set up title to accept data-binding
onOk: 'expression', // create a delegate onOk function
onCancel: 'expression', // create a delegate onCancel function
show: 'accessor' // create a getter/setter function for visibility.
}
我们通常想使用复杂一些的DOM结构来代替单一的directive。This allows the directives to become a short hand for reusable components from which applications can be built.(这句怎么翻?)
下面是一个创建可复用的widget的例子。
index.html
<script src="http://code.angularjs.org/angular-1.0.1.min.js"></script>
<script src="script.js"></script>
<div ng-controller="Ctrl3">
Title: <input ng-model="title"> <br>
Text: <textarea ng-model="text"></textarea>
<hr>
<div class="zippy" zippy-title="Details: {{title}}...">{{text}}</div>
</div>
style.css
.zippy {
border: 1px solid black;
display: inline-block;
width: 250px;
}
.zippy.opened > .title:before { content: '▼ '; }
.zippy.opened > .body { display: block; }
.zippy.closed > .title:before { content: '► '; }
.zippy.closed > .body { display: none; }
.zippy > .title {
background-color: black;
color: white;
padding: .1em .3em;
cursor: pointer;
}
.zippy > .body {
padding: .1em .3em;
}
script.js
function Ctrl3($scope) {
$scope.title = 'Lorem Ipsum';
$scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor…';
}
angular.module('zippyModule', [])
.directive('zippy', function(){
return {
restrict: 'C',
// This HTML will replace the zippy directive.
replace: true,
transclude: true,
scope: { title:'@zippyTitle' },
template: '<div>' +
'<div class="title">{{title}}</div>' +
'<div class="body" ng-transclude></div>' +
'</div>',
// The linking function will add behavior to the template
link: function(scope, element, attrs) {
// Title element
var title = angular.element(element.children()[0]),
// Opened / closed state
opened = true;
// Clicking on title should open/close the zippy
title.bind('click', toggle);
// Toggle the closed/opened state
function toggle() {
opened = !opened;
element.removeClass(opened ? 'closed' : 'opened');
element.addClass(opened ? 'opened' : 'closed');
}
// initialize the zippy
toggle();
}
}
});
End to end test
it('should bind and open / close', function() {
input('title').enter('TITLE');
input('text').enter('TEXT');
expect(element('.title').text()).toEqual('Details: TITLE…');
expect(binding('text')).toEqual('TEXT');
expect(element('.zippy').prop('className')).toMatch(/closed/);
element('.zippy > .title').click();
expect(element('.zippy').prop('className')).toMatch(/opened/);
});