이 문서의 예에서는 AngularJS에서 사용자 지정 지침을 만드는 방법을 설명합니다. 참고할 수 있도록 모든 사람과 공유하세요. 자세한 내용은 다음과 같습니다.
Angular 개발자의 지침을 번역한 것입니다. 주로 Angular 개발의 기본 사항에 이미 익숙한 개발자를 대상으로 합니다. 이 문서에서는 고유한 지침을 생성해야 하는 경우와 지침을 생성하는 방법을 설명합니다.
디렉티브란 무엇입니까?
높은 수준에서 디렉티브는 특정 태그(속성, 요소 이름 또는 설명)가 나타날 때 각도 $compile 서비스에 대한 설명입니다. DOM 시간에는 지정된 동작을 DOM에 추가하도록 컴파일러에 지시합니다.
과정은 매우 간단합니다. Angular에는 ngBind 및 ngView와 같은 많은 내장 명령어가 있습니다. 컨트롤러 및 서비스를 만드는 것처럼 자신만의 명령어도 만들 수 있습니다. Angular가 시작되면 Angular의 컴파일러는 지시문과 일치하도록 HTML을 구문 분석하여 지시문이 동작을 등록하거나 DOM을 변경할 수 있도록 합니다.
명령어 일치
명령어를 작성하기 전에 먼저 각도가 명령과 어떻게 일치하는지 알아야 합니다. 다음 예에서는 입력 요소가 ngModel 명령과 일치한다고 말합니다.
<input ng-model="foo">
다음 방법도 ngModel과 일치합니다.
<input data-ng:model="foo">
Angular는 태그를 정규화하고 어떤 요소가 어떤 지시어와 일치하는지 결정하기 위한 요소의 속성 이름입니다. js에서 표준화된 camelCase 이름을 사용하여 지시문(예: ngModel)을 참조합니다. HTML에서는 '-'로 구분된 속성 이름이 명령(예: ng-model)을 호출하는 데 사용되는 경우가 많습니다.
표준화 프로세스:
-요소 또는 속성에서 x-를 제거하고 data- prefix
-convert ':', '-' 및 '_-' 형식 이름을 Camel Case로 변환
다음 예는 ngBind 지시어와 일치하는 다양한 방법을 보여줍니다
<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/>
모범 사례: '-' 형식의 이름을 사용하는 데 우선 순위를 둡니다(예: ng-bind는 ngBind와 일치함). HTML 유효성 검사 도구를 통과하려면 'data-' 접두사(예: data-ng-bind)를 사용할 수 있습니다. 역사적인 이유로 다른 형식 이름이 존재하므로 사용하지 마세요.
$compile 서비스는 요소 이름, 속성 이름, 클래스 이름 및 주석을 기반으로 지시어를 일치시킬 수 있습니다.
Angular에서 내부적으로 제공되는 모든 지시어는 속성 이름, 태그 이름, 주석 또는 클래스 이름과 일치합니다. . 다음과 같은 다양한 방법을
<my-dir></my-dir> <span my-dir="exp"></span> <!-- directive: my-dir exp --> <span class="my-dir: exp;"></span>
으로 구문 분석할 수 있습니다. 모범 사례: 지침을 사용하려면 태그 이름과 속성 이름을 사용하는 것이 우선입니다. 이렇게 하면 지정된 요소가 어떤 요소와 일치하는지 더 쉽게 이해할 수 있습니다.
모범 사례: 주석 방법은 일반적으로 DOM API에서 테이블 요소와 같은 여러 요소에 걸쳐 있는 명령어 생성을 제한하고 반복 중첩을 제한하는 데 사용되므로 주석 방법을 사용합니다. AngularJS 버전 1.2에서는 ng-repeat-start 및 ng-repeat-end를 더 나은 솔루션으로 사용하여 이 문제를 해결했습니다. 가능하면 이 접근 방식을 권장합니다.
텍스트와 속성 바인딩
컴파일 과정에서 컴파일러는 $interpolate 서비스를 사용하여 일치하는 텍스트와 속성 값에 포함된 표현식이 포함되어 있는지 감지합니다. 이러한 표현식은 Watch로 등록되며 다이제스트 주기 중에 업데이트될 수 있습니다.
<a ng-href="img/{{username}}.jpg">Hello {{username}}!</a>
ngAttr 속성 바인딩
브라우저는 때때로 합법적이라고 간주하는 속성 값에 대해 매우 까다롭습니다(즉, 특정 일부 요소의 속성은 임의로 할당할 수 없습니다. 그렇지 않으면 오류가 보고됩니다.
예:
<svg> <circle cx="{{cx}}"></circle> </svg>
이 쓰기 방법을 사용하면 콘솔에 오류가 보고됩니다. 오류: 잘못된 값 속성 cx=" {{cx}}"입니다. 이는 SVG DOM API의 제한 때문입니다. 단순히 cx="{{cx}}"라고 쓸 수는 없습니다.
ng-attr-cx를 사용하세요. 이 문제를 해결하려면
바인딩된 속성이 ngAttr 접두사(또는 ng-attr)를 사용하는 경우 바인딩 시 해당 접두사가 없는 속성에 적용됩니다. 이 방법을 사용하면 필요한 속성에 즉시 바인딩할 수 있습니다. 브라우저에서 처리되는 속성(예: SVG 요소의 Circle[cx] 속성)
그래서 위의 문제를 해결하기 위해 다음과 같이 작성할 수 있습니다.
<svg> <circle ng-attr-cx="{{cx}}"></circle> </svg>
명령 만들기
먼저 명령어 등록을 위한 API에 대해 이야기해보겠습니다. 명령어는 모듈에 등록됩니다. 차이점은 module.directive API를 통해 명령어를 등록한다는 것입니다. module.directive는 표준화된 이름과 팩토리 함수를 허용합니다. 이 팩토리 함수는 다양한 구성이 포함된 객체를 반환합니다. 이 객체는 $compile 서비스에 다음 단계를 진행하는 방법을 알려주는 데 사용됩니다.
팩토리 함수는 컴파일러가 처음으로 명령어와 일치할 때 한 번만 호출됩니다. 일반적으로 초기화 작업은 팩토리 기능에서 수행됩니다. 이 함수는 $injector.invoke를 사용하여 호출되므로 컨트롤러처럼 의존성 주입에 사용할 수 있습니다.
모범 사례: 함수 반환보다 정의된 개체 반환을 우선시합니다.
다음으로 먼저 몇 가지 일반적인 예를 살펴본 후 다양한 구성 항목의 원리와 컴파일 프로세스에 대해 자세히 살펴보겠습니다.
Best Practice: 为了避免与某些未来的标准命名冲突,最好前缀化你自己的指令,比如你创建一个
以下的例子中,我们统一使用my前缀。
模板扩展的指令
当你有大量代表客户信息的模板。这个模板在你的代码中重复了很多次,当你改变一个地方的时候,你不得不在其他地方同时改动,这时候,你就要使用指令来简化你的模板。
我们来创建一个指令,简单的时候静态模板来替换它的内容。
<div ng-controller="Ctrl"> <div my-customer></div> </div>
JS
angular.module('docsSimpleDirective', []) .controller('Ctrl', function($scope) { $scope.customer = { name: 'Naomi', address: '1600 Amphitheatre' }; }) .directive('myCustomer', function() { return { template: 'Name: {{customer.name}} Address: {{customer.address}}' }; });
注意我们这里做了一些绑定,$compile编译链接
这个例子中,我们直接在template配置项里写上模板,但是随着模板大小的增加,这样非常不优雅。
Best Practice: 除非你的模板非常小,否则更好的是分割成单独的hmtl文件,然后使用templateUrl选项来加载。
假如你熟悉ngInclude,templateUrl跟它非常类似。现在我们使用templateUrl方式重写上面的例子:
<div ng-controller="Ctrl"> <div my-customer></div> </div>
JS:
angular.module('docsTemplateUrlDirective', []) .controller('Ctrl', 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}}
非常好,但是如果我们想让我们的指令匹配标签名
Note: 创建指令的时候,默认仅使用属性的方式。为了创建一个能由元素名字触发的指令,你需要用到restrict配置。
restrict配置可以按如下方式设置:
-'A' 仅匹配属性名字
-'E' 仅匹配元素名字
-'AE' 可以匹配到属性名字或者元素名
所以,我们可以使用 restrict: 'E'配置我们指令。
<div ng-controller="Ctrl"> <div my-customer></div> </div>
JS
angular.module('docsTemplateUrlDirective', []) .controller('Ctrl', function($scope) { $scope.customer = { name: 'Naomi', address: '1600 Amphitheatre' }; }) .directive('myCustomer', function() { return { restrict: 'E', templateUrl: 'my-customer.html' }; });
my-customer.html
Name: {{customer.name}} Address: {{customer.address}}
Note: 什么时候使用属性名或元素名呢? 当创建一个含有自己模板的组件的时候,需要使用元素名,如果仅仅是为已有的元素添加功能的话,使用属性名。
使用元素名做为myCustomer指令是非常正确的决定,因为你不是用一些'customer'行为来点缀元素,而是定义一个具有自己行为的元素作为customer组件。
隔离指令的作用域
上面我们的myCustomer指令已经非常好了,但是它有个致命的缺陷,我们在给定的作用域内仅能使用一次。
它现在的实现是,我们每次重用该指令的时候都要为它新创一个控制器。
<div ng-controller="NaomiCtrl"> <my-customer></my-customer> </div> <hr> <div ng-controller="IgorCtrl"> <my-customer></my-customer> </div>
JS
angular.module('docsScopeProblemExample', []) .controller('NaomiCtrl', function($scope) { $scope.customer = { name: 'Naomi', address: '1600 Amphitheatre' }; }) .controller('IgorCtrl', function($scope) { $scope.customer = { name: 'Igor', address: '123 Somewhere' }; }) .directive('myCustomer', function() { return { restrict: 'E', templateUrl: 'my-customer.html' }; });
my-customer.html
Name: {{customer.name}} Address: {{customer.address}}
这很明显不是一个好的解决方案。
我们想要做的是能够把指令的作用域与外部的作用域隔离开来,然后映射外部的作用域到指令内部的作用域。可以通过创建isolate scope来完成这个目的。这样的话,我们使用指令的scope配置。
<div ng-controller="Ctrl"> <my-customer customer="naomi"></my-customer> <hr> <my-customer customer="igor"></my-customer> </div>
JS
angular.module('docsIsolateScopeDirective', []) .controller('Ctrl', function($scope) { $scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' }; $scope.igor = { name: 'Igor', address: '123 Somewhere' }; }) .directive('myCustomer', function() { return { restrict: 'E', scope: { customer: '=customer' }, templateUrl: 'my-customer.html' }; });
my-customer.html
Name: {{customer.name}} Address: {{customer.address}}
首先看hmtl,第一个
现在看看scope是如何配置的。
//... scope: { customer: '=customer' }, //...
属性名(customer)是myCustomer指令上isolated scope的变量名。它的值(=customer)告诉编译器绑定到customer属性。
Note: 指令作用域配置中的'=attr'属性名是被规范化过后的名字,比如要绑定
对于属性名和你想要绑定的值的名字一样,你可以使用这样的快捷语法:
//... scope: { // same as '=customer' customer: '=' }, //...
使用isolated scope还有另外一个用处,那就是可以绑定不同的数据到指令内部的作用域。
在我们的例子中,我们可以添加另外一个属性vojta到我们的作用域,然后在我们的指令模板中访问它。
<div ng-controller="Ctrl"> <my-customer customer="naomi"></my-customer> </div>
JS
angular.module('docsIsolationExample', []) .controller('Ctrl', function($scope) { $scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' }; $scope.vojta = { name: 'Vojta', address: '3456 Somewhere Else' }; }) .directive('myCustomer', function() { return { restrict: 'E', scope: { customer: '=customer' }, templateUrl: 'my-customer-plus-vojta.html' }; });
my-customer-plus-vojta.html
Name: {{customer.name}} Address: {{customer.address}} <hr> Name: {{vojta.name}} Address: {{vojta.address}}
注意到,{{vojta.name}}和{{vojta.address}}都是空的,意味着他们是undefined, 虽然我们在控制器中定义了vojta,但是在指令内部访问不到。
就像它的名字暗示的一样, 指令的isolate scope隔离了除了你添加到作用域:{}对象中的数据模型外的一切东西。这对于你要建立一个可重复使用的组件是非常有用的,因为它阻止了除了你想要传入的数据模型外其他东西改变你数据模型的状态。
Note: 正常情况下,作用域是原型继承自父作用域。但是isolate scope没有这样的继承。
Best Practice: 当你想要使你的组件在应用范围内可重用,那么使用scope配置去创建一个isolate scopes
创建一个操作DOM的指令
在这个例子中,我们会创建一个显示当前时间的指令,每秒一次更新DOM以正确的显示当前的时间。
指令修改DOM通常是在link配置中,link选项接受一个带有如下标签的函数function link(scope,element,attrs) {...}其中:
-scope是angular scope对象
-element指令匹配的jqLite封装的元素(angular内部实现的类jquery的库)
-attrs是一个带有规范化后属性名字和相应值的对象
在我们的link函数中,我们更新显示时间每秒一次,或者当用户改变指定绑定的时间格式字符串的时候。我们也要移除定时器,当指令被删除的时候,以避免引入内存泄露。
<div ng-controller="Ctrl2"> Date format: <input ng-model="format"> <hr/> Current time is: <span my-current-time="format"></span> </div>
JS
angular.module('docsTimeDirective', []) .controller('Ctrl2', function($scope) { $scope.format = 'M/d/yy h:mm:ss a'; }) .directive('myCurrentTime', function($timeout, 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(); }); function scheduleUpdate() { // save the timeoutId for canceling timeoutId = $timeout(function() { updateTime(); // update DOM scheduleUpdate(); // schedule the next update }, 1000); } element.on('$destroy', function() { $timeout.cancel(timeoutId); }); // start the UI update process. scheduleUpdate(); } return { link: link }; });
这里有很多东西值得注意的,就像module.controller API, module.directive中函数参数是依赖注入,因此,我们可以在Link函数内部使用$timeout和dataFilter服务。
我们注册了一个事件element.on('$destroy', ...), 是什么触发了这个$destory事件呢?
AngularJS会触发一些特定的事件,当一个被angular编译过的DOM元素被移除的时候,它会触发一个$destory事件,同样的,当一个angular作用域被移除的时候,它会向下广播$destory事件到所有监听的作用域。
通过监听事件,你可以移除可能引起内存泄露的事件监听器,注册在元素和作用域上的监听器在它们被移除的时候,会自动会清理掉,但是假如注册一个事件在服务或者没有被删除的DOM节点上,你就必须手工清理,否则会有内存泄露的风险。
Best Practice:执行被移除的时候应该做一些清理的操作, 可以使用element.on('$destroy', ...)或者scope.on('$destroy', ...)来运行解除绑定的函数,
创建包裹其他元素的指令
我们现在已经实现了,使用isolate scopes传递数据模型到指令里面。但是有时候我们需要能够传递一整个模板而不是字符串或者对象。让我们通过创建'dialog box'组件来说明。这个'dialog box'组件应该能够包裹任意内容。
要实现这个,我们使用transclude配置
<div ng-controller="Ctrl"> <my-dialog>Check out the contents, {{name}}!</my-dialog> </div>
JS
angular.module('docsTransclusionDirective', []) .controller('Ctrl', function($scope) { $scope.name = 'Tobias'; }) .directive('myDialog', function() { return { restrict: 'E', transclude: true, templateUrl: 'my-dialog.html' }; });
my-dialog.html
<div class="alert" ng-transclude> </div>
这个transclude配置用来干嘛呢? transclude使带有这个配置的指令的内容能够访问指令外部的作用域。
参照以下例子,注意到我们增加了一个link函数,在这个link函数内部我们重定义了name属性的值为Jeff,那么现在这个{{name}}会被解析成哪个值呢?
<div ng-controller="Ctrl"> <my-dialog>Check out the contents, {{name}}!</my-dialog> </div>
JS
angular.module('docsTransclusionDirective', []) .controller('Ctrl', function($scope) { $scope.name = 'Tobias'; }) .directive('myDialog', function() { return { restrict: 'E', transclude: true, templateUrl: 'my-dialog.html', link: function (element, scope) { scope.name = 'Jeff'; } }; });
my-dialog.html
<div class="alert" ng-transclude> </div>
一般,我们会认为{{name}}会被解析为Jeff,然而这里,我们看到这个例子中的{{name}}还是被解析成了Tobias.
transclude配置改变了指令相互嵌套的方式,他使指令的内容拥有任何指令外部的作用域,而不是内部的作用域。为了实现这个,它给指令内容一次访问外部作用域的机会。
这样的行为对于包裹内容的指令是非常有意义的。因为如果不这样的话,你就必须分别传入每个你需要使用的数据模型。如果你需要传入每个要使用的数据模型,那么你就无法做到适应各种不同内容的情况。
Best Practice: 仅当你要创建一个包裹任意内容的指令的时候使用transclude:true。
下一步,我们增加一个按钮到'dialog box'组件里面,允许用户使用指令绑定自己定义的行为。
<div ng-controller="Ctrl"> <my-dialog ng-hide="dialogIsHidden" on-close="dialogIsHidden = true"> Check out the contents, {{name}}! </my-dialog> </div>
JS
angular.module('docsIsoFnBindExample', []) .controller('Ctrl', function($scope, $timeout) { $scope.name = 'Tobias'; $scope.hideDialog = function () { $scope.dialogIsHidden = true; $timeout(function () { $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()">×</a> <div ng-transclude></div> </div>
我们想要通过在指令的作用域上调用,来运行我们传递进去的函数,但是这个函数是运行在定义时候的上下文(js通常都是这样子的)。
先前我们看到如何scope配置使用'=prop',但是在上文的例子中,我们使用'&prop','&'绑定开放了一个函数到isolated scope,允许 isolated scope调用它,同时维持原来函数的作用域(这里的作用域都是指$scope)。所以当一个用户点击x时候,就会运行Ctrl控制器的close函数。
Best Practice: 当你的指令想要开放一个API去绑定特定的行为,在scope配置中使用'&prop'.
创建一个添加事件监听器的指令
先前,我们使用link函数创建一个操作DOM元素的指令,基于上面的例子,我们创建一个在元素上添加事件监听的指令。
举个例子,假如我们想要创建一个让用户可拖拽的元素,该怎么做呢?
Drag ME
JS
angular.module('dragModule', []). directive('myDraggable', function($document) { return 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.screenX - x; startY = event.screenY - y; $document.on('mousemove', mousemove); $document.on('mouseup', mouseup); }); function mousemove(event) { y = event.screenY - startY; x = event.screenX - startX; element.css({ top: y + 'px', left: x + 'px' }); } function mouseup() { $document.unbind('mousemove', mousemove); $document.unbind('mouseup', mouseup); } } });
创建相互通信的指令
你可以通过在模板使用指令来组合任何指令。
有时候,你想要一个指令从其他的指令上面创建
想象你想要一个带有tab的容器,容器的内容对应于激活的tab。
<my-tabs> <my-pane title="Hello"> <h5 id="creating-custom-directives_source_hello">Hello</h5> <p>Lorem ipsum dolor sit amet</p> </my-pane> <my-pane title="World"> <h5 id="creating-custom-directives_source_world">World</h5> <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>
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>
myPane指令有一个require:'^myTabs'的配置,当指令使用这个配置,$compile服务叫myTabs的指令并获取它的控制器实例,如果没有找到,将会抛出一个错误。'^'前缀意味着指令在它的父元素上面搜索控制器(没有'^'前缀的话,指令默认会在自身的元素上面搜索指定的指令)。
这里myTabs的控制器是来自何处呢?通过使用controller配置可以为指令指定一个控制器, 上问例子中myTab就是使用这个配置。就像ngController, 这个配置为指令的模板绑定了一个控制器。
再看我们的myPane's定义,注意到link函数的最后一个参数: tabCtrl,当一个指令包含另一个指令(通过require方式),它会接收该指令的控制器实例作为link函数的第四个参数,利用这个,myPane可以调用myTabs的addPane函数。
精明的读者可能想知道link跟controller之间的区别,最基本的区别就是控制器开放一个API(就是这个控制器实例可以被其他实例读取到),link函数可以通过require与控制器交互。
Best Practice: 当你要开放一个API给其他指令的时候使用控制器,否则使用link函数。
总结
这里我们讲解了一个些指令的主要使用案例。每一个都可以作为你创建自己指令的很好的起点。
如果你想更深入的了解编译的处理过程,可以查看compiler guide相关内容
$compile API页面有directive每个配置项的具体解释,可以参阅。
希望本文所述对大家AngularJS程序设计有所帮助。