angularjs指令
2017-08-06 03:00 蝶梦中 阅读(192) 评论(0) 编辑 收藏 举报这篇文档阐述了怎样在你的angularjs程序中如何建立和执行自定义指令。
什么是指令?
总起来说,指定就是建立在DOM元素上的标记(例如一个属性,元素名称,注释或是CSS类),它可以告知angularjs的HTML编译器($compile)在DOM元素上添加一个特定的行为(例如,事件监听),甚至改变DOM元素及其子元素。
Angularjs包含很多内嵌的指令集,比如ngBind,ngModel和ngClass。就像你创建控制器和服务一样,你可以创建angularjs可用的自定义应用。当angularjs引导你的程序是,HTML编译器会遍历DOM元素来匹配指令。
HTML模板中的编译意味着什么?对angularjs来说,“编译”意味着添加指令到HTML使其可交互。我们使用“编译”这个术语的原因是绑定指令的递归过程类似于编译式语言的编译源代码。
匹配指令
在开始写指令之前,我们需要知道angularjs HTML编译器如何决定何时来使用一个给定指令的。
类似于用来表述元素匹配选择器的术语,当一个指令是声明的一部分时我们称之为一个元素匹配这个指令。
在下面的例子中,<input>元素匹配ngModel指令
<input ng-model="foo">
下面的<input>元素同样匹配ngModel:
<input data-ng-model="foo">
下面的<person>元素匹配person指令:
<person>{{name}}<person>
标准化
AngularJS会标准化处理一个元素的标签和属性名称来实现元素和指令的一一对应。通常来说我们使用区分大小写的驼峰拼写法来标准化一个指令名称(例如,ngModel)。但是HTML时不区分大小写的,我们在html中使用小写字母和中划线来在HTML中显示指令(如ng-model)。
标准化过程如下所示:
1.去除元素/属性前面的x-和data-
2.将:,-或是_分割的形式转换为驼峰命名法
index.html
<!doctype html>
<html ng-app="docsBindExample">
<head>
<script src="http://code.angularjs.org/1.2.25/angular.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> <br/>
<span ng:bind="name"></span> <br/>
<span ng_bind="name"></span> <br/>
<span data-ng-bind="name"></span> <br/>
<span x-ng-bind="name"></span> <br/>
</div>
</body>
</html>
script.js
angular.module('docsBindExample', []).controller('Controller', ['$scope', function($scope) {
$scope.name = 'Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)';
}]);
最佳实践:最好是使用中划线分割的形式。如果你需要使用一个HTML验证工具,你可以使用data-在前面的形式。其他的形式是合法的但是我们不建议使用。
指令类型
编译可以使用元素名称(E),元素属性(A),类型名称(C)和注释(M)来匹配指令。
下面示范了在一个模板里四种不同的匹配指令的方式(指令为myDir)。
<my-dir></my-dir>
<span my-dir=“exp"></span>
<!— directive: my-dir exp —>
<span class=“my-dir:exp;”></span>
一个指令可以在指令定义对象的restrict属性中指定支持哪几种匹配类型。默认值为EA。
最佳实践:相比于通过注释和类名,人们更喜欢通过标签名称和属性来使用指令。这样操作通常可以使指令更简单的匹配元素。
最佳实践:注释指令通常在DOM API限制跨越元素但是指令需要跨越元素时使用(例如table)。Angularjs引入了 ng-repeat-start和ng-repeat-end来更好的解决这个问题。建议开发者使用这个而不是注释指令。
创建指令
首先来讨论一下注册指令的接口。类似于控制器,指令在模块中注册。你需要使用module.directive接口来注册一个指令。module.directive使用工厂化方法来标准化指令名称。这个工厂化方法会返回一个包含不同选项的对象来告诉编译器这个指令在匹配成功时会有什么样的行为。
当编译器第一次匹配到指令时,工厂方法被唤醒。你可以在这执行任何初始化工作。这个方法使用$indector.invoke来执行唤醒操作,这使得它像一个控制器一样被注入。
我们要重温一些指令常见的例子,然后深入到不同的选项和编译过程。
最佳实践:为了防止跟未来的一些标准有冲突,最好给指令名称加上你独有的前缀。举例来说,如果你创建了一个<carousel>指令,如果HTM7颁布了新的同名元素,这里就会产生问题。两三个字母的前缀能很好解决这个问题(如<btfCarousel>)。同样的,不要使用ng或是其他可能冲突的字符来作为你私人指令的前缀,他们可能与未来angularjs的版本冲突。
在下列例子中,我们将要使用前缀my。
模板扩展指令
目前你模板的很大一块代码用来表示一个客户的信息。这个模板在你的代码中重复了很多次。当你在一个地方修改他时,其他的也会被同步修改。这是一个使用指令来简化你的模板的好机会。
script.js
angular.module('docsSimpleDirective', []).controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};}]).directive('myCustomer', function() {
return {
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};});
index.html
<div ng-controller="Controller"> <div my-customer></div></div>
result
Name: Naomi Address: 1600 Amphitheatre
在上面的例子中我们在template选项中插入了数值,然而当模板变大后这会变得很烦人。
最佳实践:除非你的模板特别小,要不然最好使用templeteUrl选项来代替直接放入HTML文件。
script.js
angular.module('docsTemplateUrlDirective', []).controller('Controller', ['$scope', function($scope) {
$scope.customer = {name: 'Naomi',address: '1600 Amphitheatre'
};
}]).directive('myCustomer', function() {
return {
templateUrl: 'my-customer.html'
};
});
index.html
<div ng-controller="Controller"> <div my-customer></div></div>
my-customer.html
Name: {{customer.name}} Address: {{customer.address}}
Name: Naomi Address: 1600 Amphitheatre
templateUrl也可以是一个返回URL的方法。AngularJS会使用两个参数来调取templateUrl函数:指令绑定的元素,和该元素的attr对象。
提示:在scope被初始化之前调用模板,是无法从template函数中获取scope 变量值的。
script.js
angular.module('docsTemplateUrlDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.directive('myCustomer', function() {
return {
templateUrl: function(elem, attr) {
return 'customer-' + attr.type + '.html';
}
};
});
index.html
<div ng-controller="Controller">
<div my-customer type="name"></div>
<div my-customer type="address"></div>
</div>
customer-name.html
customer-address.html
Name: {{customer.name}}
Address: {{customer.address}}
提示:当你创建一个指令时,默认映射到属性和元素。如果你需要使用类名称来触发所建的自定义服务,你需要使用restrict选项。
restrict选项一般设置为:
'A'-只匹配属性名称
'E'-只匹配元素名称
'C'-只匹配类型名称
'M'-只匹配注释
这些约束条件可以结合到一起:'AEC'-匹配属性名称或是元素名称或是类型名称
让我们修改指令使其使用 restrict: 'E'
script.js
angular.module('docsRestrictDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.directive('myCustomer', function() {
return {
restrict: 'E',
templateUrl: 'my-customer.html'
};
});
index.html
<div ng-controller="Controller"> <my-customer></my-customer> </div>
my-customer.html
Name: {{customer.name}} Address: {{customer.address}}
我们什么情况下需要使用元素或是属性?如果选择使用元素,我们需要新建一个组件来控制模板。通用的做法是在模板中创建一个领域专用语言。使用属性创建指令就是给一个已知元素添加新的方法。
使用元素来创建myCustomer指令很明显是一个正确的选择,因为你不需要给一个元素添加多种"customer"行为,你需要为一个'customer'组件添加关键行为。
给指令一个独立作用域
我们的myCustomer指令已经很好了,但是还有一个致命的弱点。我们在所设置的作用域中只能使用一次。在当前的视线中,我们如果想重用这个指令需要创建一个不同的控制器。
script.js
angular.module('docsScopeProblemExample', [])
.controller('NaomiController', ['$scope', function($scope) {
$scope.customer = {
name: 'Naomi',
address: '1600 Amphitheatre'
};
}])
.controller('IgorController', ['$scope', function($scope) {
$scope.customer = {
name: 'Igor',
address: '123 Somewhere'
};
}])
.directive('myCustomer', function() {
return {
restrict: 'E',
templateUrl: 'my-customer.html'
};
});
index.html
<div ng-controller="NaomiController"> <my-customer></my-customer> </div> <hr> <div ng-controller="IgorController"> <my-customer></my-customer> </div>
my-customer.html
Name: {{customer.name}} Address: {{customer.address}}
Name: Naomi Address: 1600 Amphitheatre
Name: Igor Address: 123 Somewhere
这很明显不是一个很好的解决方案。我们需要将指令的作用域和外侧域隔离开来,然后将外侧域映射到一个指令的作用域。我们可以通过创建独立作用域来实现这个功能。为了实现这个需求,我们可以使用一个指令的scope选项:
script.js
angular.module('docsIsolateScopeDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
$scope.igor = { name: 'Igor', address: '123 Somewhere' };
}])
.directive('myCustomer', function() {
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
templateUrl: 'my-customer-iso.html'
};
});
index.html
<div ng-controller="Controller"> <my-customer info="naomi"></my-customer> <hr> <my-customer info="igor"></my-customer> </div>
my-customer-iso.html
Name: {{customerInfo.name}} Address: {{customerInfo.address}}
Name: Naomi Address: 1600 Amphitheatre
Name: Igor Address: 123 Somewhere
请看index.html,就像控制器域中揭露的那样,第一个<my-customer>元素为info绑定了naomi属性,第二个info绑定了igor。
让我们仔细查看一下域选项:
//...
scope: {
customerInfo: '=info'
},
//...
- 它的名字(customerInfo)与指令中独立作用域的customerInfo属性相符。
- 它的值info告诉编译器跟info属性绑定
请注意:这些指令域选项中的=attr属性和指令名称有相同的标准化规则。你需要定义绑定为=bindToThis而使他绑定到<div bind-to-this=“thing”>的属性中。
如果是被绑定的属性名称跟指令域中的属性名称一致,你可以使用缩写句法:
...
scope: {
// same as '=customer'
customer: '='
},
...
除了在指令的域中绑定不同的数据,使用独立作用域还有另一个作用。我们可以通过添加另一个属性vojta到我们的域中然后砸在我们指令的模板中尝试访问来展示。
script.js
angular.module('docsIsolationExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
$scope.vojta = { name: 'Vojta', address: '3456 Somewhere Else' };
}])
.directive('myCustomer', function() {
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
templateUrl: 'my-customer-plus-vojta.html'
};
});
index.html
<div ng-controller="Controller"> <my-customer info="naomi"></my-customer> </div>
My-customer-plus-vojta.html
Name: {{customerInfo.name}} Address: {{customerInfo.address}}
<hr>
Name: {{vojta.name}} Address: {{vojta.address}}
Result
Name: Naomi Address: 1600 Amphitheatre
Name: Address
请注意{{Volta.name}}和{{Volta.address}}都是空的,这意味着它们是未定义的。即使我们在控制器中定义了vojta,在指令中它是不可用的。
就像名字所建议的那样,指令的独立定义域隔离了除了添加到scope:{}中哈希对象之外的所有数据模型。这个在建立可复用的组件时是有用的,因为它组织了你添加之外的数据模型修改模型状态。
请注意:一般来说,一个域继承了它父亲的原型。但是一个独立作用域并不这样。
最佳实践:如果你想在程序中复用组件时,使用选项scope来创建独立作用域。
创建一个可操作dom的指令
在这个例子里,我们将要建立一个展示当前时间的指令。每过一秒,它更新这个DOM来修改当前时间。
指令通常使用link选项来注册DOM监听器和更新DOM。他会在包含指令逻辑的模版被复制后执行。
link使用下面的签名,function link(scope, element,attrs,controller,transcludeFn){…}:
- Scope是一个Angularjs的域对象。
- element是指令匹配的元素。
- attrs是一个包含标准属性名称和相匹配的属性值的哈希对象。
- controller是指令所需求的控制器实例或是它独有的控制器。具体的数据取决于指令所需要的属性。
- transcludeFn是一个转义链接函数预先绑定到正确的跨越域。
更多详情请查看api文档中的link选项。
在link函数中,我们需要每秒更新一次展示时间,同时也需要在用户修改绑定到指令的时间格式是更新展示时间。我们需要使用$interval服务来调用一个定期处理函数。当我们想要确定所有的$timeout函数在结束测试前结束时,这比$timeout函数更使用简单。当指令被删除时我们同样需要移除$interval来避免一个内存泄漏。
script.js
angular.module('docsTimeDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.format = 'M/d/yy h:mm:ss a';
}])
.directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) {
function link(scope, element, attrs) {
var format,
timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
element.on('$destroy', function() {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function() {
updateTime(); // update DOM
}, 1000);
}
return {
link: link
};
}]);
index.html
<div ng-controller="Controller">
Date format: <input ng-model="format"> <hr/>
Current time is: <span my-current-time="format"></span>
</div>
我们注册了element.on(‘destroy’,…)时间,是什么触发了$destroy事件?
这里有一些angularjs的特殊函数。当一个被angularjs编译器编译的的dom节点被销毁时,它触发了$destroy事件。类似的,当一个angularjs域被销毁时,它向所有监听域广播一个$destroy事件。
监听到该事件后,我们可以删除可能导致内存溢出的监听事件。注册到域和元素的监听器会在它们被销毁时自动消除,不过如果你给服务注册了一个监听器,或是给一个未被删除的dom节点注册了一个监听器,你需要自己清除这些监听器,不然可能会发生内存泄漏。
最佳实践:指令应该添加自动清除功能。你可以使用element.on(‘$destroy’,…)或是scope.$on(‘$destroy’,…)以在指令被移除后运行一个消除函数。
创建一个包含其他元素的指令
我们已经注意到可以使用独立作用域来让指令访问数据模型,不过很多时候我们需要访问整个模板而不是一个字符串或是一个对象。我们需要创建一个”对话框”组件。这个对话框需要可以包含随意内容。
我们需要使用transclude选项来完成该功能。
Script.js
angular.module('docsTransclusionDirective', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: 'my-dialog.html'
};
});
index.html
<div ng-controller="Controller"> <my-dialog>Check out the contents, {{name}}!</my-dialog> </div>
my-dialog.html
<div class="alert" ng-transclude></div>
为了更好的阐述这个,请查看下面的例子。注意到我们在script.js中添加了定义name为jeff的link函数。你觉着绑定的{{name}}会怎么解决?
script.js
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: 'my-dialog.html',
link: function(scope) {
scope.name = 'Jeff';
}
};
});
index.html
<div ng-controller="Controller"> <my-dialog>Check out the contents, {{name}}!</my-dialog> </div>
My-dialog.html
<div class="alert" ng-transclude></div>
transclude选项改变了域的嵌套方式。包含transclude的指令先匹配指令外部的域而不是指令内部的域。它赋予了指令访问外部域内容的权限。如果指令没有创建其独有域,scope.name=‘jeff’中的scope会影响外部域,我们会看到输出jeff。
这种行为使指令包含一些内容变得简单,因为如果不这样做你需要在单独的赋予访问每个数据模型的权限,这样你不可能真正包含随意的内容。
最佳实践:只有需要包含任意内容时才需要使用transclude:true。
下面我们添加按钮到这个对话框中,同时添加绑定个人行为到这个指令的许可。
script.js
angular.module('docsIsoFnBindExample', [])
.controller('Controller', ['$scope', '$timeout', function($scope, $timeout) {
$scope.name = 'Tobias';
$scope.message = '';
$scope.hideDialog = function(message) {
$scope.message = message;
$scope.dialogIsHidden = true;
$timeout(function() {
$scope.message = '';
$scope.dialogIsHidden = false;
}, 2000);
};
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {
'close': '&onClose'
},
templateUrl: 'my-dialog-close.html'
};
});
index.html
<div ng-controller="Controller">
{{message}}
<my-dialog ng-hide="dialogIsHidden" on-close="hideDialog(message)">
Check out the contents, {{name}}!
</my-dialog>
</div>
My-dialog-close.html
我们想要在指令域中触发运行这个函数。不过在这个域注册位置的上下文运行他。
<div class="alert"> <a href class="close" ng-click="close({message: 'closing for now'})">×</a> <div ng-transclude></div> </div>
我们可以看到在域选项中使用=attr是很容易的,不过在上面的选项中,我们使用&attr。绑定&允许指令在初始域中在特定时间里使用表达式。任何合法的表达式都是允许的,包括包含一个函数调用的表达式。因为这个,&绑定是将回调函数绑定到指令的理想选择。
当用户点击该对话框的x的时候,指令里的close函数被调用。它在独立作用域中调用表达式中等于原始域中hideDialog(message)的close函数。然后运行控制器中的hideDialog函数。
独立作用域使用表达式来从父作用域中获取数据是经常需要用到的。这个可以在表达式任意函数中包含本地变量名称和值。举例来说,这个hideDialog函数在对话框被关闭时显示提示信息。这个先在指令中调用close{{message:’closing for now’}}。然后本地变量message在on-close表达式中会变成可用。
最佳实践:只有当你希望在指令上添加一个绑定行为的接口时才在scope选项中使用&attr。
创建添加事件监听器的指令
在上文中,我们使用link函数来创建修改dom元素的指令。基于这个例子,让我们创建一个影响该元素的事件的指令。
举例来说,怎么创建一个可以让用户移动元素的指令?
script.js
angular.module('dragModule', [])
.directive('myDraggable', ['$document', function($document) {
return {
link: function(scope, element, attr) {
var startX = 0, startY = 0, x = 0, y = 0;
element.css({
position: 'relative',
border: '1px solid red',
backgroundColor: 'lightgrey',
cursor: 'pointer'
});
element.on('mousedown', function(event) {
// Prevent default dragging of selected content
event.preventDefault();
startX = event.pageX - x;
startY = event.pageY - y;
$document.on('mousemove', mousemove);
$document.on('mouseup', mouseup);
});
function mousemove(event) {
y = event.pageY - startY;
x = event.pageX - startX;
element.css({
top: y + 'px',
left: x + 'px'
});
}
function mouseup() {
$document.off('mousemove', mousemove);
$document.off('mouseup', mouseup);
}
}
};
}]);
Index.html
<span my-draggable>Drag Me</span>
创建可以通信的指令
你可以在模版中使用任何指令来创建新指令。有时候,你需要一个由指令组合构件的组件。想象一下,你需要一个包含选项卡的容器,容器显示被激活的选项的内容。
script.js
angular.module('docsTabsExample', [])
.directive('myTabs', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
controller: ['$scope', function MyTabsController($scope) {
var panes = $scope.panes = [];
$scope.select = function(pane) {
angular.forEach(panes, function(pane) {
pane.selected = false;
});
pane.selected = true;
};
this.addPane = function(pane) {
if (panes.length === 0) {
$scope.select(pane);
}
panes.push(pane);
};
}],
templateUrl: 'my-tabs.html'
};
})
.directive('myPane', function() {
return {
require: '^^myTabs',
restrict: 'E',
transclude: true,
scope: {
title: '@'
},
link: function(scope, element, attrs, tabsCtrl) {
tabsCtrl.addPane(scope);
},
templateUrl: 'my-pane.html'
};
});
index.html
<my-tabs> <my-pane title="Hello"> <p>Lorem ipsum dolor sit amet</p> </my-pane> <my-pane title="World"> <em>Mauris elementum elementum enim at suscipit.</em> <p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p> </my-pane> </my-tabs>
My-tabs.html
My-pane.html
myPane指令含有一个值为^^myTabs的require选项。当一个指令使用该选项时,编译器在未发现该特定控制器时会抛出错误。^^前缀意味着该指令在父节点中搜索这个控制器(一个^前缀表示指令会在它自身元素和父节点中搜寻控制器,没有任何前缀表示指令只会在它自身的元素中搜寻控制器)。
<div class="tabbable"> <ul class="nav nav-tabs"> <li ng-repeat="pane in panes" ng-class="{active:pane.selected}"> <a href="" ng-click="select(pane)">{{pane.title}}</a> </li> </ul> <div class="tab-content" ng-transclude></div> </div>
<div class="tab-pane" ng-show="selected"> <h4>{{title}}</h4> <div ng-transclude></div> </div>
myTabs控制器从哪里来呢?使用controller选项可以很容易创建一个控制器。如你所见,myTabs指令使用了该选项。就像ngController一样,这个选项在指令中添加了一个控制器。
如果需要在模板中引用这个控制器或是绑定到该控制器的绑定函数,你可以使用controllerAs选项来为给定控制器指定别名。这个指令需要定义这个配置的适用范围。当指令被作为一个组件使用时这个功能会特别有用。
我们回到myPane的定义,来看Link函数的最后一个参数:tabsCtrl。当一个指令需要控制器时,它在link函数中接受这个控制器为第四个参数。通过这个改进,myPane可以调用myTabs的addPane函数。
如果需要多个控制器,指令的require选项可以使用一个参数数组。发送到Link的通信参数也会是一个数组。
angular.module('docsTabsExample', [])
.directive('myPane', function() {
return {
require: ['^^myTabs', 'ngModel'],
restrict: 'E',
transclude: true,
scope: {
title: '@'
},
link: function(scope, element, attrs, controllers) {
var tabsCtrl = controllers[0],
modelCtrl = controllers[1];
tabsCtrl.addPane(scope);
},
templateUrl: 'my-pane.html'
};
});
聪明的读者可能会奇怪link和controller之间的区别。最基本的区别是controller可以导出一个API,而link函数可以通过使用require来跟controller做交互。
最佳实践:当你想要导出一个API到其他指令时,使用controller。其他情况下使用link。
总结
在这里我们看到了指令的大多数应用方式。每一个都可以作为你创建自己指令的很好的起点。
如果你想更深入的了解编译的处理过程,可以查看HTML编译器。