AngularJs自定義指令詳解(5) - link
在指令中操作DOM,我們需要link參數,這參數要求聲明一個函數,稱之為鏈接函數。
寫法:
link: function(scope, element, attrs) {
// 在這裏操作DOM
}
如果指令使用了require選項,那麽鏈接函數會有第四個參數,代表控制器或者所依賴的指令的控制器。
// require ‘SomeController‘,
link: function(scope, element, attrs, SomeController) {
// 在這裏操作DOM,可以訪問required指定的控制器
}
鏈接函數之所以能夠在指令中操作DOM,就是因為傳入的這幾個參數:scope、element、attrs
scope:即與指令元素相關聯的當前作用域,可以用來註冊監聽器:scope.$watch()
element:即當前指令對應的元素,使用它可以操作該元素及其子元素。例如<span my-directive></span>,這個span就是指令 my-directive所使用的元素。
attrs:由當前元素的屬性組成的對象。
下面看一個例子,來自官方文檔的示例。
我們要實現一個時鐘,根據給定的時間格式顯示當前的時間,而且每隔一秒要更新一次時間。
首先在控制器中初始化一個時間格式:
controller(‘Controller‘, [‘$scope‘, function($scope) {
$scope.format = ‘M/d/yy h:mm:ss a‘;
}])
對於時間格式,顯然我們要引入$filter服務。
對於”每隔一秒“進行某些操作,顯然要引入$interval服務。
為了測試程序,我們還引入$log服務以便在瀏覽器中觀察輸出。
所以自定義的指令需要寫成這樣:
directive(‘myClock‘, [‘$interval‘, ‘$filter‘, ‘$log‘, function($interval, $filter,$log)
這個myClock指令將會被註入$interval、$filter、$log服務。
刷新時間顯示,也就是要求我們在指令中操作DOM,輸出時間:
function updateTime() {
element.text($filter(‘date‘)(new Date(), interFormat));
}
$filter方法的使用:
$filter(‘date‘)(date, format, timezone)
參考https://code.angularjs.org/1.3.16/docs/api/ng/filter/date
每隔一秒刷新顯示:
$interval(
function() {
updateTime();
},
1000
);
完整代碼如下:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <script src="../lib/angular-1.3.16/angular.min.js"></script> <script src=""></script> <title></title> <script language="JavaScript"> angular.module(‘docsTimeDirective‘, []) .controller(‘Controller‘, [‘$scope‘, function($scope) { $scope.format = ‘M/d/yy h:mm:ss a‘; }]) .directive(‘myClock‘, [‘$interval‘, ‘$filter‘, ‘$log‘, function($interval, $filter,$log) { return { scope:{ myFormat:‘=‘ }, link: function(scope, element, attrs) { function updateTime() { element.text($filter(‘date‘)(new Date(), scope.myFormat)); } updateTime(); $interval(function() { updateTime(); }, 1000); } }; }]); </script> </head> <body ng-app="docsTimeDirective"> <div ng-controller="Controller"> 時間格式: <input ng-model="format"> <hr/> 當前時間: <span my-clock my-format="format"></span> </div> </body> </html>
運行效果:
不過我們很快就發現一個問題,就是修改時間格式後,無法立刻刷新時間顯示,把每隔一秒修改為每隔五秒,問題就更加明顯了。
雖然修改format會因為雙向綁定而使myFormat發生變化,但是後者並不會觸發執行updateTime()函數。
所以需要加入$watch監聽:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <script src="../lib/angular-1.3.16/angular.min.js"></script> <script src=""></script> <title></title> <script language="JavaScript"> angular.module(‘docsTimeDirective‘, []) .controller(‘Controller‘, [‘$scope‘, function($scope) { $scope.format = ‘M/d/yy h:mm:ss a‘; }]) .directive(‘myClock‘, [‘$interval‘, ‘$filter‘, ‘$log‘, function($interval, $filter,$log) { return { scope:{ myFormat:‘=‘ }, link: function(scope, element, attrs) { function updateTime() { element.text($filter(‘date‘)(new Date(), scope.myFormat)); } scope.$watch(‘myFormat‘,function(newValue) { $log.info(‘value changed to ‘ + newValue); updateTime(); }); $interval(function() { updateTime(); }, 1000); } }; }]); </script> </head> <body ng-app="docsTimeDirective"> <div ng-controller="Controller"> 時間格式: <input ng-model="format"> <hr/> 當前時間: <span my-clock my-format="format"></span> </div> </body> </html>
註意$watch()的第一個參數為‘myFormat‘,不要少了單引號,也不要寫成‘scope.myFormat‘、‘$scope.myFormat‘,要不然newValue的值是undefined了。
還有就是刪掉了外面的updateTime()調用,因為在$watch裏myFormat第一次綁定時,已經觸發監聽器的回調函數了,於是updateTime()也立刻執行。
上面的代碼監聽的是定義在指令的隔離作用域上的myFormat,而官方文檔監聽的是DOM中span元素的my-format屬性,效果是差不多的,代碼如下:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <script src="../lib/angular-1.3.16/angular.min.js"></script> <script src=""></script> <title></title> <script language="JavaScript"> angular.module(‘docsTimeDirective‘, []) .controller(‘Controller‘, [‘$scope‘, function($scope) { $scope.format = ‘M/d/yy h:mm:ss a‘; }]) .directive(‘myClock‘, [‘$interval‘, ‘$filter‘, ‘$log‘, function($interval, $filter,$log) { return { link: function(scope, element, attrs) { var interFormat, timeoutId; function updateTime() { element.text($filter(‘date‘)(new Date(), interFormat)); } scope.$watch(attrs.myFormat, function(value) { interFormat = value; updateTime(); }); element.on(‘$destroy‘, function() { $interval.cancel(timeoutId); }); timeoutId = $interval(function() { updateTime(); }, 1000); } }; }]); </script> </head> <body ng-app="docsTimeDirective"> <div ng-controller="Controller"> 時間格式: <input ng-model="format"> <hr/> Current time is: <span my-clock my-format="format"></span> </div> </body> </html>
官方文檔指出一個問題:$interval註冊的匿名函數不會在元素被移除時自動釋放,存在一定的內存泄露風險,所以增加了代碼:
element.on(‘$destroy‘, function() {
$interval.cancel(timeoutId);
});
這三行代碼也演示了如何在指令內加入對元素的事件監聽器,官方文檔還提供了另一個例子,代碼如下:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <script src="../lib/angular-1.3.16/angular.min.js"></script> <script src=""></script> <title></title> <script language="JavaScript"> 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 blue‘, backgroundColor: ‘yellow‘, 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); } } }; }]); </script> </head> <body ng-app="dragModule"> <span my-draggable>Drag ME</span> </body> </html>
現在回顧一下前面文章提到的隔離作用域問題,看看以下代碼:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <script src="../lib/angular-1.3.16/angular.min.js"></script> <script src=""></script> <title></title> <script language="JavaScript"> angular.module(‘app‘,[]) .directive(‘myDirective‘,function(){ return{ template:‘Hello {{greeting}}!‘, //scope:{ }, link:function(scope,element,attrs){ scope.greeting = ‘AngularJs‘; } }; }); </script> </head> <body ng-app="app"> <div ng-init="greeting=‘World‘"></div> 1,<span>Hello {{greeting}}!</span><hr> 2,<span my-directive></span><hr> </body> </html>
輸出:
1,Hello AngularJs!
2,Hello AngularJs!
雖然greeting被初始化為‘World‘,但是在鏈接函數裏修改成‘AngularJs’,可見此時傳給鏈接函數的scope是上一級作用域(在這裏是rootScope)
這造成了汙染,一般情況下我們不希望指令不聲不響地修改外面的變量,解決辦法是把代碼裏//scope:{}的註釋去掉,隔離指令的作用域。
於是輸出就會變成:
1,Hello World!
2,Hello AngularJs!
再看下面的代碼:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <script src="../lib/angular-1.3.16/angular.min.js"></script> <script src=""></script> <title></title> <script language="JavaScript"> angular.module(‘app‘,[]) .directive(‘myDirective‘,function(){ return{ restrict:‘E‘, template:‘<span ng-transclude></span>‘, scope:{ }, transclude: true, link:function(scope,element,attrs){ scope.greeting = ‘AngularJs‘; } }; }); </script> </head> <body ng-app="app"> <div ng-init="greeting=‘World‘"></div> 1,<span>Hello {{greeting}}!</span><hr> 2,<my-directive>Hello {{greeting}}!</my-directive><hr> </div> </body> </html>
輸出:
1,Hello World!
2,Hello World!
即使ng-transclude指令放在指令定義的模板中,但是{{greeting}}綁定放在外面,而指令已經隔離了作用域,所以{{greeting}}使用的是外面的‘World‘。
如果註釋掉scope:{},指令的作用域沒有隔離,於是輸出就變為:
1,Hello AngularJs!
2,Hello AngularJs!
AngularJs自定義指令詳解(5) - link