[译] 如何使用 TypeScript 编写自定义 AngularJS 指令?
原文链接 : How to write custom AngularJS Directive using TypeScript?
原文作者 : Siddharth Pandey
译者 : 李林璞(web前端领域)
译者注:翻译如有疏漏,欢迎指出!感谢!转载请保留此头部。
AngularJS 框架有很多强大的特性,其中指令(Directives)是广为人知的。在这篇文章中,我将告诉你如何用 TypeScript 编写自定义 AngularJS 指令。首先,我将讲一下关于指令的基本知识,但如果你想直接看 TypeScript 代码的,你可以跳过基本。
什么是指令?
在较高层面上,指令是一个 DOM 元素的标记(像一个属性,元素名称,注释或者 CSS 类),它告诉 Angular 的 HTML 编译器(
$compiler
)去给 DOM 元素连接一个特殊的行为(例如通过事件监听),或者甚至是改变这个 DOM 元素和其子元素。Angular 本身有一些内建的指令,像
ngbind
,ngModel
和ngClass
。就像你创建控制器(controllers)和服务(services)那样,你可以创建自己的指令给 Angular 使用。当 Angular 启动你的应用时,HTML 编译器就会对 DOM 元素进行遍历找到符合的指令。
例子
看看ng-controller
和ng-bind
这些 AngularJS 框架自带指令的使用方法:
<div ng-controller="Controller"> Hello <input ng-model='name'> <hr/> <span data-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>
标准化过程
上面的代码片段,有多种方式去标记一个指令。AngularJS 的 HTML 编译器负责决定哪个元素匹配哪个指令,一般通过区分大小写的 camelCase
(驼峰式) 命名方法(如 ngModel
)去使用指令。但是,因为 HTML 是不区分大小写的,一般我们使用小写形式的 短横线-分隔 属性写在 DOM 元素上(如ng-model
)。
标准化过程如下:
前面的元素或属性使用带
x-
或data-
的形式。将
:
,-
或_
这些分隔符转化为camelCase
(驼峰式)。
最佳实践
最好使用 短横线-分隔 格式(如 ng-bind
对应 ngbind
)。如果你想使用一个 HTML 验证工具,可以使用 带data前缀 的版本进行替代(如 data-ng-bind
对应 ngbind
)。上面展示的其他形式因遗留原因仍然可以使用当还是建议避免使用它们。
指令的类型
AngularJS 的 HTML 编译器如$compiler
可以基于元素名,属性,类名,还有注释去匹配指令。下面的例子展示了一个名为myDir
的指令在一个 HTML 模板里是怎么用多种方式去引用的:
<my-dir></my-dir> <span my-dir="exp"></span> <!-- directive: my-dir exp --> <span class="my-dir: exp;"></span>
最佳实践
最好通过标签名或者属性而不是注释和类名去使用指令。这样做通常会更容易去决定哪个指令匹配给定的元素。
注释指令通常在 DOM API 限制跨越多个元素创建指令的地方使用(如table
元素)。AngularJS 1.2 采用了ng-repeat-start
和ng-repeat-end
作为解决这个问题更好的方法,鼓励开发者尽量使用这个方法代替注释指令。
TypeScript 中的自定义指令
让我们来创建一个只为任何的块,小部件或者人名在右边添加标题,子标题和文本的指令。这是一个很好的例子,因为它可以在很多地方重用而且可以作为一个有隔离作用域的指令在每个动态加载的块中作为信息展示。
来看看 HTML,Typescript 的代码在其下方:
<div class="widget-head"> <div class="page-title pull-left">{{title}}</div> <small class="page-title-subtle" ng-show="subtitle">({{subtitle}})</small> <div class="widget-icons pull-right"></div> <small class="pull-right page-title-subtle" ng-show="rightText">{{rightText}}</small> <div class="clearfix"></div> </div> interface IHtWidgetHeaderScope { title: string; subtitle: string; rightText: string; allowCollapse: string; } //Usage: //<div ht-widget-header title="vm.map.title"></div> // Creates: // <div ht-widget-header="" // title="Movie" //</div> class HtWidgetHeader implements ng.IDirective { static $inject: Array<string> = ['']; constructor() { } static instance(): ng.IDirective { return new HtWidgetHeader(); } scope: IHtWidgetHeaderScope = { 'title': '@', 'subtitle': '@', 'rightText': '@' }; templateUrl: string = 'app/widgets/widget-header.html'; restrict: string = 'EA'; } angular.module('app').directive('htWidgetHeader', HtWidgetHeader.instance);
利用 TypeScript 的特点,创建一个定义了可在指令内使用的作用域成员的接口(interface)。同样地我们想创建一个指令的实例,我们就定义一个实现了IDirective
的类(class)。
要想知道类型定义,看看这个令人吃惊的仓库,它收集了几乎所有流行的 JavaScript 库。这些类型定义可以让我们得到任何编译时错误和 IDE 的智能支持。我使用 Visual Studio 和 Visual Code,它们都对 TypeScript 有很好的支持。
这个指令不需要任何内建的 angular 服务或任何依赖,所以 $inject
这个静态成员只是一个空数组。如果依赖被列出来的话,框架就会根据这个变量的内容去寻找然后依赖注入。
构造器(constructor)不用做什么事情但我们还是需要一个静态的 instance
方法去创建一个指令的实例。框架会在定义一个使用了模块指令 API 的时候期望一个指令的实例。
这个类的 scope
在这里非常重要,因为这个指令使用隔离的作用域,即它自身的成员变量可以在这个指令的模板当中使用但并不继承外层或其父级作用域的声明。为了可读性和可维护性,我们使用了 templateUrl
去指定模板的源码。另外,restrict
设置了指令的使用级别给元素和属性,分别使用 E 和 A 表示。
restrict
选项一般设置为:
'A':只匹配属性名
'E':只匹配元素名
'C':只匹配类名
'M':只匹配注释
这些限制只要需要都可以结合:'AEC' 匹配属性或元素或类名。
现在可以像下面的代码片段那样使用这个指令:
<div ht-widget-header title="{{vm.title}}" subtitle="{{vm.description}}" right-text="{{vm.refreshedDateTimeInfo}}"></div>
这个指令可以在给到一个硬编码或者动态的 title
,subtitle
或者 right-text
作用域成员的情况下作为元素或者属性使用。注意到后者和任何指令一样都已经被编译器标准化。上面的代码片段在一个模板里使用,该模板链接到一个含有 title
,description
和 refreshedDateTimeInfo
变量的控制器,然后展示给用户。给定一些标记和设计,就会像下面这样:
如果你想学到更多有关如何整合 AngularJS 和 TypeScript 的知识,可以看看我的 AngularJS 文章。如果你想学习其他一些特别的东西可以联系我,我会尝试写相关文章的。