创建自定义指令

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

什么是指令?

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

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

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)时使用的术语类似,当指令是其声明的一部分时,我们说元素匹配指令。

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

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

以下<input>元素也匹配ngModel:

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

以下<person>元素与person指示相匹配:

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

Normalization (暂译 规范化)

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

规范化过程如下:
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>
翻译不易,若您觉得对您有帮助,欢迎打赏

微信:《创建自定义指令》

支付宝:《创建自定义指令》

    原文作者:TommY
    原文地址: https://segmentfault.com/a/1190000015084337
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞