建立自定义指令

文档翻译至angularjs.org. 文档解释了您什么时候想在AngularJS应用程序中建立本身的指令,以及如何实现它们。 | 建议搭配原文食用 |html

什么是指令?

在高层次上,指令时DOM元素上的标记(做为属性,元素名,注释和CSS类)用来告诉Angularjs的HTML Compiler($compile)附加特定的行为在此DOM元素上(例如,经过事件监听),或者甚至去转换DOM元素和他的子元素。angularjs

Angularjs附加了一系列内建的执行,像ngBind, ngModel, and ngClass. 和你建立的控制器和服务同样,你能够建立你本身的指令来供Angularjs使用. 当Angularjs 启动(bootstraps)你的应用时,HTML compiler遍历DOM匹配DOM元素对应的指令。bootstrap

What does it mean to "compile" an HTML template? For AngularJS, "compilation" means attaching directives to the HTML to make it interactive. The reason we use the term "compile" is that the recursive process of attaching directives mirrors the process of compiling source code in compiled programming languages.数组

指令匹配
在咱们开始写指令以前, 咱们须要知道当使用一个给定的指令, Angularjs的 HTML Compiler是如何判断的微信

与元素匹配选择器(element matches a selector)时使用的术语相似,当指令是其声明的一部分时,咱们说元素匹配指令。app

在下面的例子中,咱们说<input>元素匹配ngModel指令less

<input ng-model="foo"> <!-- as an attr -->

如下<input>元素也匹配ngModel:ide

<input data-ng-model="foo">

如下<person>元素与person指示相匹配:函数

<person>{{name}}</person>

Normalization (暂译 规范化)

AngularJS规范化元素的标签和属性名称,以肯定哪些元素与哪些指令相匹配。咱们一般经过其区分大小写的camelCase规范化名称(例如,ngModel)来定义(refer to)指令。然而,因为HTML是大小写不敏感的,咱们经过小写形式在DOM中引用指令,一般使用dash(-)分割符分割不一样的单词(例如ng-model)post

规范化过程以下:
1.剔除 元素/属性开头的 x- , data- ;
2.将 - , _ ,: 分隔符转换为小驼峰式 camelCas
例如,如下形式都是等同的,而且与ngBind指令相匹配:

<div ng-controller="Controller">
  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>

指令类型

A - attributes    <div person> </div>
C - class name    <div class="person"> </div>
E - element name   <person></person>
M - comments    <!-- directive: person -->

Best Practice: Prefer using directives via tag name and attributes over comment and class names.Doing so generally makes it easier to determine what directives a given element matches.

Best Practice: Comment directives were commonly used in places where the DOM API limits the ability to create directives that spanned multiple elements (e.g. inside <table> elements). AngularJS 1.2 introduces ng-repeat-start and ng-repeat-end as a better solution to this problem. Developers are encouraged to use this over custom comment directives when possible..

建立指令

首先让咱们讨论下注册指令的API(API for registering directives). 和控制器同样,指令也是注册在模块之上的。为了注册一个指令,你须要使用 module.directive API。module.directive接受标准化的指令名称,后跟一个工厂函数。这个工厂函数应该返回一个具备不一样选项的对象来告诉$compile指令在匹配时应该如何表现。

当$conpile第一次匹配指令时,工厂函数仅被调用一次。你能够在这里指令任意的初始化工做。该(工厂)函数使用 $injector.invoke 来调用这使得它能够像控制器同样是可注射。

咱们将经过一些常见的指令示例,而后深刻探讨不一样的选项和编译过程。

Best Practice: In order to avoid collisions with some future standard, it's best to prefix your own directive names. For instance, if you created a <carousel> directive, it would be problematic if HTML7 introduced the same element. A two or three letter prefix (e.g. btfCarousel) works well. Similarly, do not prefix your own directives with ng or they might conflict with directives included in a future version of AngularJS.

做为后续示例,咱们将使用my前缀(例如 myCustomer)。

Template-expanding 指令

假设您有一大块表明客户信息的模板。这个模板在您的代码中重复屡次。当你在一个地方改变它时,你必须在其余几个地方改变它。这是使用指令简化模板的好机会。

让咱们建立一个指令,用一个静态模板简单地替换它的内容:
https://jsfiddle.net/TommyLee...

注意咱们在这个指令中有bindings。在$compile编译和连接<div my-customer></div>后,它将会尝试在元素的子元素上匹配指令。这意味着你能够组建指令的指令(嵌套指令)。咱们将在后续看到如何编写 an example 。

在上面的例子中,咱们列出了模板选项(template attribute of return object in factory function),但随着模板大小的增加,这将变得使人讨厌。

Best Practice: Unless your template is very small, it's typically better to break it apart into its own HTML file and load it with the templateUrl option.

若是你熟悉ngInclude,templateUrl就像它同样工做。下面是使用templateUrl代替的相同示例:
https://plnkr.co/edit/idFOZ8Q...

templateUrl也能够是一个函数,它返回要加载和用于指令的HTML模板的URL。AngularJS将使用两个参数调用templateUrl函数:指令被调用的元素以及与该元素相关联的attr对象。

Note: You do not currently have the ability to access scope variables from the templateUrl function, since the template is requested before the scope is initialized
注:(要访问socpe上的值,应该在post-link阶段).

https://plnkr.co/edit/gaSYwnp...

restrict 选项一般设置为:
图片描述

When should I use an attribute versus an element? Use an element when you are creating a component that is in control of the template.The common case for this is when you are creating a Domain-Specific Language for parts of your template. Use an attribute when you are decorating an existing element with new functionality.

用元素来使用myCustomer指令时明智的选择,由于你不用一些“customer”行为修饰一个元素,你定义一个元素核心行为做为一个costomer组建。

隔离指令的Scope

咱们以上的myCustomer指令很好,可是它有一个致命缺陷。咱们只有在一个给定的scope下使用。

在其目前的实现上,咱们应该须要去建立一些不一样点控制器用来重用这个指令。
https://plnkr.co/edit/CKEgb1e...

这明显不是一个好的解决方案。

咱们说项的是把指令内部的scope与外部scope(controller scope)分离,而且映射外部scope到指令内部scope。咱们能够经过建立一个isolate scope来作。为此,咱们可使用指令的scope选项。

https://plnkr.co/edit/E6dTrgm...
看index.html文件,第一个<my-customer>元素绑定info属性值为naomi,它是咱们已经暴露在咱们的控制器上的scope。第二个绑定info为igor。

让咱们仔细看看scope选项

//... 
scope: { customerInfo: '=info' },
//...

除了能够将不一样的数据绑定到指令中的做用域外,使用isolated scope还有其余做用。

咱们能够经过添加另外一个属性vojta来展现,到咱们的scope并尝试从咱们的指令模板中访问它:
https://plnkr.co/edit/xLVqnzt...

请注意{{vojta.name}}和{{vojta.address}}为空,意味着它们未定义(undefined)。虽然咱们在控制器中定义了vojta,但它在指令中不可用。

顾名思义,该指令的 isolate scope隔离了除显式添加到做用域的模型以外的全部内容:scope: {}散列对象. 这在构建可重用组件时颇有用,由于它能够防止组件改变模型状态,除了显式传入。

Note: Normally, a scope prototypically inherits from its parent. An isolated scope does not. See the "Directive Definition Object - scope"section for more information about isolate scopes.

Best Practice: Use the scope option to create isolate scopes when making components that you want to reuse throughout your app.

建立一个操纵DOM的指令

在这个例子中,咱们将创建一个显示当前时间的指令。每秒一次,它会更新DOM以反映当前时间。

想要修改DOM的指令一般使用link选项来注册DOM监听器以及更新DOM。它在模板被克隆以后执行,而且是放置指令逻辑的地方。

link接受一个带有一下签名的函数function link(scope, element, attrs, controller, transcludeFn) { ... }, 其中:

  • scope是一个Angularjs scope 对象
  • element 是一个此指令匹配的jqLite包装元素
  • attrs是一个具备标准化属性名称及其对应属性值的键值对的散列对象。
  • controller是指令所需的控制器实例或其本身的控制器(若是有的话)。确切的值取决于指令的 require属性。
  • transcludeFn是预先绑定到正确的包含范围的transclude连接函数。

For more details on the link option refer to the $compile API page.

在咱们的link函数中,咱们想每秒钟更新显示时间,或者一个用户改变了咱们指令绑定的时间格式字符串。咱们将会使用$interval服务按期调用处理程序。这比使用$ timeout更容易,但对于端到端测试也更好,咱们但愿确保在完成测试以前完成全部$timeout。若是指令被删除,咱们也想删除$ interval,因此咱们不会引入内存泄漏
https://plnkr.co/edit/vIhhmNp...

这里有几件事须要注意。就像module.controller API同样,module.directive中的函数参数是依赖注入的。所以,咱们能够在指令的连接函数中使用$ interval和dateFilter。

咱们注册一个事件element.on('$ destroy',...)。什么引起了这个$ destroy事件?

AngularJS发布了一些特殊事件。当用AngularJS的编译器编译的DOM节点被销毁时,它会发起$ destroy事件。一样,当Angularjs scope被销毁,他会广播(broadcasts)一个$destory事件监听scopes。

经过监听此事件,能够删除可能致使内存泄漏的事件侦听器。注册到scope和element的监听事件在销毁DOM时会自动清理,可是若是您在服务上注册了侦听器,或者在未被删除的DOM节点上注册了侦听器,你必须本身清理它,不然你有冒险引入内存泄漏的风险。

Best Practice: Directives should clean up after themselves. You can use element.on('$destroy', ...) or scope.$on('$destroy', ...) to run a clean-up function when the directive is removed.

建立包装其余元素的指令

咱们已经看到,您可使用isolate scope将模型传递给指令,可是有时候想要能传入一整个模板而不是一个字符串或者对象。咱们说咱们想要建立一个“dialog box”组建。dialog box应该有能力包装任意的内容(any arbitrary content)。

为此,咱们须要使用transclude选项。
https://plnkr.co/edit/empMwVW...

transclude选项到底作了什么呢?transclude使指令的内容经过此选项拥有可访问外部指令的scope不是内部的scope。

为了说明了这一点,请看下面的例子。注意,咱们在script.js中添加了一个link函数,将名称从新定义为Jeff。您认为{{name}}绑定将会获得什么结果?
https://plnkr.co/edit/OEdkXY4...

照常,咱们觉得{{name}}应该是Jeff。可是,咱们看见的是Tobias。

transclude选项改变了scope的嵌套方式。它使得一个transcluded指令的内容具备在指令以外的任何scope内容,而不是任何内部的scope。这样作,它可让内容访问外部scope。

请注意,若是指令没有建立本身的独立做用域,那么scope.name ='Jeff'中的做用域将引用外部做用域,咱们会在输出中看到Jeff。

这种行为对于封装某些内容的指令是有意义的,由于不然,您必须分别传入每一个您想要使用的模型。若是你必须传入每个你想要的model,那么你不能真正的使用任意的内容,对吗?

Best Practice: only use transclude: true when you want to create a directive that wraps arbitrary content.

接下来,咱们要在此对话框中添加按钮,并容许使用该指令的用户将本身的行为绑定到该对话框。
https://plnkr.co/edit/Bo5lona...

咱们但愿经过从指令的做用域调用它来运行咱们传递的函数,可是它会在注册做用域的上下文中运行。

咱们在以前已经看到在scope选项中如何使用 =attr,可是在上面的例子中,咱们使用了&attr代替。 &绑定容许一个指令去触发一个原始范围内的表达式的评估,在一个特定时间点上。任何合法的表达式都是容许的,包括一个含有函数调用的表达式。如此,& 绑定是理想的将回调函数绑定到指令行为。

当用户点击dialog中的 x,指令的close函数被调用,多亏于ng-click。这个close调用在isolated scope之上,实际上会在原始scope的上下文中评估表达式 hideDialog(message),致使运行Controller中的hideDialog function。

一般指望经过一个表达式从isolate scope传入数据到父scope,这能够经过将局部变量名称和值的映射传递到表达式包装函数来完成。例如,hideDialkog函数接受一个message来显示当dialog被隐藏是。这被指令调用 close({message: 'closing for now'})指明。接着局部变量message将在on-close表达式内被访问(is available).

Best Practice: use &attr in the scope option when you want your directive to expose an API for binding to behaviors.

建立一个添加事件监听器的指令

之前,咱们使用连接函数来建立操纵其DOM元素的指令。在这个例子的基础上,让咱们制定一个对其元素事件作出反应的指令。

例如,若是咱们想建立一个容许用户拖拽元素的指令呢?
https://plnkr.co/edit/hcUyuBY...

建立一个通讯的指令

你能够组建任何指令经过模板使用他们。

有时,你须要一个由指令组合构建的组件。

想象你想要有一个容器,其中容器的内容对应于哪一个选项卡处于活动状态的选项卡。
https://plnkr.co/edit/kqLjcwG...

myPane指令有require选项值为^^myTabs. 当指令使用此选项,&compile将抛出一个错误除非特定的controller被找到。 ^^前缀表示该指令在其父元素上搜索控制器。(^前缀将使指令在自身元素或她的父元素上寻找控制器;又没任何前缀,指令将值操做自身)

因此这个myTabs contoller从哪里来的?指令能够特定一个controllers经过使用 controller选项。如你所见,myTabs指令使用了此选项。就像ngController,此选项附加一个控制器到指令的模板上。

若是须要从模板中引用控制器或绑定到控制器的任何功能,则可使用选项controllerAs将控制器的名称指定为别名。该指令须要定义要使用的此配置的范围。这在指令被用做组件的状况下特别有用。

回头看myPane的定义,注意到link函数的最后一个参数:tabCtrl。当指令须要控制器时,它将接收该控制器做为其link函数的第四个参数。利用这一点,myPane能够调用myTabs的addPane函数。

若是须要多个控制器,则指令的require选项能够采用数组参数。发送给连接函数的相应参数也将是一个数组。

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'
  };
});

明的读者可能想知道连接和控制器之间的区别。基本的区别是控制器能够暴露一个API,而且连接函数可使用require与控制器交互。

Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.

总结

到此咱们已经看了大多数指令的用法,每个样例演示了一个建立你本身指令的好的起始点。

你可能深刻感兴趣于编译过程的解释能够在这里得到compiler guide.

$compile API 有一个全面的指令清单选项以供参考。

最后

若有任何问题和建议欢迎发送至邮箱讨论:<Tommy.White.h.li@gmail.com>
翻译不易,若您以为对您有帮助,欢迎打赏

微信:图片描述

支付宝:图片描述

相关文章
相关标签/搜索