在前端开发的过程中,很多时候我们都希望能够重复的使用某一个模块,比如说文件上传、组织架构树等,angular的directive可以帮助我们轻松的实现组件的重复使用。
首先先让我们解释一个问题:什么是directive?
directive是DOM元素的一个标记(可以是属性、元素名称、类名和文本内容),告诉angular的compiler给这个元素添加指定的行为,或者改变这个元素及其子元素。
angular提供了很多内置的directive(比如ngBind、ngModel和ngClass)方便我们使用,就像可以创建controller和service一样,我们也可以创建自己的directive。
下面让我们创建一个directive,这个directive仅仅用一个静态的模板替换它的内容。
index.html
<div ng-controller="Controller">
<div my-customer></div>
</div>
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}}'
};
});
展示结果
Name: Naomi Address: 1600 Amphitheatre。
当然,很多时候我们要在directive中展示的内容,绝不仅仅是一个静态的文本,可能是一颗组织架构树,有着复杂的样式,这时候将html代码写在directive的template属性后就显得代码很臃肿,我们可以使用templateUrl属性,后面跟上需要加载的模板路径,例如示例所示:
index.html
<div ng-controller="Controller">
<div my-customer></div>
</div>
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'
};
});
my-customer.html
Name: {{customer.name}} Address: {{customer.address}}
需要说明的是,templateUrl后面可以是一个方法,方法返回需要加载的模板路径,例如:
.directive('myCustomer', function() {
return {
templateUrl: function(elem, attr){
return 'customer-'+attr.type+'.html';
}
};
});
该方法后面可以跟两个参数,elem代表匹配到directive的元素,attr代表和元素关联的对象。
当我们创建一个directive,这个directive默认按照元素和属性进行匹配,我们可以通过restrict属性进行匹配设置。
restrict
属性可以设置为:
'A'
- 只匹配属性名称'E'
- 只匹配元素名称'C'
- 只匹配样式名称
如果需要可以结合使用:
'AEC'
- 同时匹配属性、元素和样式名称。
.directive('myCustomer', function() {
return {
restrict: 'E',
templateUrl: 'my-customer.html'
};
});
我们上面的directive写的很棒,但是有一个致命的缺陷,就是我们的directive不能重复使用,换句话说,就是在一个指定的scope中,由于directive直接访问controller中的customer,所以directive替换之后的结果是一样,通过下面的示例简单说明一下:
index.html
<div ng-controller="NaomiController">
<my-customer></my-customer></br>
<my-customer></my-customer>
</div>
展示结果:
Name: Naomi Address: 1600 Amphitheatre。
Name: Naomi Address: 1600 Amphitheatre。
解决问题的方法是将directive中的scope与外面的scope隔离,同时将需要在directive中使用的值映射到directive的scope中,我们将这种解决方法称为isolate scope,下面,我们还是通过一个示例简单演示一下:
index.html
<div ng-controller="Controller">
<my-customer info="naomi"></my-customer>
<hr>
<my-customer info="igor"></my-customer>
</div>
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'
};
});
my-customer-iso.html
Name: {{customerInfo.name}} Address: {{customerInfo.address}}
展示结果
Name: Naomi Address: 1600 Amphitheatre
Name: Igor Address: 123 Somewhere
在index.html中我们将naomi(定义在controller中:$scope.naomi)绑定在info属性上面,在我们的directive中,我们通过customerInfo: '=info',将naomi绑定到customerInfo中,因此我们可以在template中通过customerInfo访问用户信息。
scope: {
// same as '=customer'
customer: '='
},
这是一种简写形式,等价于customer: '=customer'。
angular的宗旨之一是将业务逻辑和页面展示分离,业务逻辑放在controller中,页面展示放在template中,controller中是不推荐直接操作DOM的,所有的DOM操作应该放在directive中进行,下面我们还是用一个简单的示例演示如何在directive中操作DOM,该示例在页面中显示一个时钟,该时钟每一秒更新一下时间。
index.html
<div ng-controller="Controller">
Date format: <input ng-model="format"> <hr/>
Current time is: <span my-current-time="format"></span>
</div>
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
};
}]);
前面我们学习了可以通过isolate scope传递值或者对象供directive使用,但有些时候我们需要传递整个模板,这时候我们需要使用transclude属性选项,考虑下面代码的输出结果是什么:
index.html
<div ng-controller="Controller">
<my-dialog>Check out the contents, {{name}}!</my-dialog>
</div>
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, element) {
scope.name = 'Jeff';
}
};
});
my-dialog.html
<div class="alert" ng-transclude>
</div>
我们在controller和directive的link方法中都定义了name属性,根据前面讲的isolate scope的知识,似乎显示的结果应该是Jeff,但是,如果你这样想就错啦,因为transclude属性修改了scope的嵌入方式,使得在directive的内容中能够访问所有外部的scope变量而不是内部的scope变量,所以最后的显示结果为:
Check out the contents, Tobias!
下面我们在对话框中添加一个按钮,允许用户绑定特定的行为:
index.html
<div ng-controller="Controller">
{{message}}
<my-dialog ng-hide="dialogIsHidden" on-close="hideDialog(message)">
Check out the contents, {{name}}!
</my-dialog>
</div>
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'
};
});
my-dialog-close.html
<div class="alert">
<a href class="close" ng-click="close({message: 'closing for now'})">×</a>
<div ng-transclude></div>
</div>
展示结果:
在上面的示例中,当我们点击×,通过ng-click调用close方法,'&'允许在directive中调用方法close,但是在close方法注册的源scope中执行,也就是在controller中执行hideDialog方法,此外可以通过键值对的形式向directive外部传递参数,如示例中所示ng-click="close({message: 'close for now'})",此时可以在hideDialog方法中访问message变量。
下面我们创建一个directive,这个directive允许用户拖拽元素,让我们以此来说明如何在directive中创建事件监听。
index.html
<span my-draggable>Drag ME</span>
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);
}
}
};
}]);
最后让我们创建一个directive,此directive根据我们点击的tab展示不同的内容。
index.html
<my-tabs>
<my-pane title="Hello">
<h4>Hello</h4>
<p>Lorem ipsum dolor sit amet</p>
</my-pane>
<my-pane title="World">
<h4>World</h4>
<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>
script.js
angular.module('docsTabsExample', [])
.directive('myTabs', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
controller: function($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'
};
});
my-tabs.html
<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>
my-pane.html
div class="tab-pane" ng-show="selected" ng-transclude>
</div>
当我们在directive中使用require属性,如果没有找到指定的controller,$compile将会报错,^前缀指明在父controller中寻找,没有^前缀则在自己的元素中查找。
如果directive中指定了require属性,那么可以在其link方法中将该controller作为第四个参数传入。如果需要多个controller,那么require属性后面可以跟一个数组,同样link方法的第四个参数传入也是一个数组,
传入需要的controller列表。