# 三、指令

指令是 Angular 中最强大的概念之一，因为它们允许您创建特定于应用程序的自定义 HTML 元素。这允许您开发可重用的组件，这些组件封装复杂的 DOM 结构、样式表甚至行为。

## 有条件地启用/禁用 DOM 元素

### 问题

您希望根据复选框状态禁用按钮。

### 溶液

使用`ng-disabled`指令并将其条件绑定到复选框状态:

```js
    <body ng-app>
    <label><input type="checkbox" ng-model="checked"/>Toggle Button</label>
    <button ng-disabled="checked">Press me</button>
    </body>

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter3/recipe1) 上找到完整的例子。

### 讨论

`ng-disabled`指令是禁用的 HTML 属性的直接翻译，无需担心浏览器不兼容。它使用属性值绑定到`checked`模型，就像使用`ng-model`指令的复选框一样。事实上，`checked`属性值又是一个 Angular 表达式。例如，你可以颠倒逻辑，用`!checked`来代替。

这只是 Angular 附带的指令的一个例子。还有很多其他的，例如，`ng-hide`、`ng-checked,`或`ng-mouseenter.`我鼓励你浏览[应用程序编程接口(API)参考](http://docs.angularjs.org/api)并探索 Angular 必须提供的所有指令。

在接下来的食谱中，我们将集中于实现指令。

## 响应用户动作改变 DOM

### 问题

您希望通过单击鼠标来更改 HTML 元素的 CSS，并将这种行为封装在一个可重用的组件中。

### 溶液

实现一个指令`my-widget`，该指令包含一个您想要设置样式的文本示例段落:

```js
    <body ng-app="MyApp">
    <my-widget>
    <p>Hello World</p>
    </my-widget>
    </body>

```

在指令实现中使用链接函数来更改段落的 CSS:

```js
    var app = angular.module("MyApp", []);

    app.directive("myWidget", function() {
    var linkFunction = function(scope, element, attributes) {
    var paragraph = element.children()[0];
    $(paragraph).on("click", function() {
    $(this).css({ "background-color": "red" });
    });
    };

    return {
    restrict: "E",
    link: linkFunction
    };
    });

```

单击段落时，背景颜色变为红色。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter3/recipe2) 上找到完整的例子。

### 讨论

在 HTML 文档中，使用新的指令作为 HTML 元素`my-widget`，在 JavaScript 代码中可以再次找到`myWidget`。指令函数返回一个限制和一个链接函数。

该限制意味着该指令只能用作 HTML 元素，而不能用作 HTML 属性。如果您想将其用作 HTML 属性，请将`restrict`改为返回`A`。这种用法必须适应:

```js
    <div my-widget>
    <p>Hello World</p>
    </div>

```

是否使用属性或元素机制将取决于您的用例。一般来说，人们会使用元素机制来定义定制的可重用组件。每当您想要配置某个元素或使用更多行为来增强它时，都会使用属性机制。其他可用选项是将指令用作类属性或注释。

`directive`方法需要一个可用于依赖项初始化和注入的函数:

```js
    app.directive("myWidget", function factory(injectables) {
    // ...
    }

```

链接函数更有趣，因为它定义了实际的行为。范围、实际的 HTML 元素`my-widget,`和 HTML 属性作为参数传递。请注意，这与 Angular 的依赖注入机制无关。参数的排序很重要！

首先，我们使用由元素定义的 Angular 的`children()`函数选择段落元素，它是`my-widget`元素的子元素。在第二步中，我们使用 jQuery 绑定到点击事件，并在点击时修改`css`属性。这是特别有趣的，因为我们这里混合了 Angular 元素函数和 jQuery。事实上，在引擎盖下，如果定义了`children()`功能，Angular 将使用 jQuery，否则将退回到 jqLite(与 Angular 一起发货)。您可以在元素的 [API 引用中找到所有支持的方法。](http://docs.angularjs.org/api/angular.element)

仅使用 jQuery 对代码进行轻微修改后:

```js
    element.on("click", function() {
    $(this).css({ "background-color": "red" });
    });

```

在这种情况下，`element`已经是 jQuery 元素了，我们可以直接使用`on`函数。

## 在指令中呈现 HTML 片段

### 问题

您希望将 HTML 片段呈现为可重用的组件。

### 溶液

实现一个指令，使用`template`属性定义 HTML:

```js
    <body ng-app="MyApp">
    <my-widget/>
    </body>

    var app = angular.module("MyApp", []);

    app.directive("myWidget", function() {
    return {
    restrict: "E",
    template: "<p>Hello World</p>"
    };
    });

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter3/recipe3) 上找到完整的例子。

### 讨论

这将把 Hello World 段落渲染为`my-widget`元素的子节点。如果要用段落完全替换元素，还必须返回`replace`属性:

```js
    app.directive("myWidget", function() {
    return {
    restrict: "E",
    replace: true,
    template: "<p>Hello World</p>"
    };
    });

```

另一个选择是为 HTML 片段使用一个文件。在这种情况下，您需要使用`templateUrl`属性，例如，如下所示:

```js
    app.directive("myWidget", function() {
    return {
    restrict: "E",
    replace: true,
    templateUrl: "widget.html"
    };
    });

```

`widget.html`应该与`index.html`文件位于同一目录中。只有当您使用 web 服务器来托管文件时，这才会起作用。GitHub 上的示例再次使用 angular-seed 作为引导。

## 渲染指令的 DOM 节点子节点

### 问题

您的小部件使用指令元素的子节点来创建组合呈现。

### 溶液

将`transclude`属性与`ng-transclude`指令一起使用:

```js
    <my-widget>
    <p>This is my paragraph text.</p>
    </my-widget>

    var app = angular.module("MyApp", []);

    app.directive("myWidget", function() {
    return {
    restrict: "E",
    transclude: true,
    template: "<div ng-transclude><h3>Heading</h3></div>"
    };
    });

```

这将呈现包含`h3`元素的`div`元素，并将指令的子节点附加到下面的段落元素。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter3/recipe4) 上找到完整的例子。

### 讨论

在这种情况下，转换是指通过引用将一个文档的一部分包含到另一个文档中。`ng-transclude`属性应该根据您希望子节点附加到的位置来定位。

## 使用 HTML 属性传递配置参数

### 问题

您希望传递一个配置参数来更改呈现的输出。

### 溶液

使用基于属性的指令并为配置传递属性值。该属性作为参数传递给链接函数:

```js
    <body ng-app="MyApp">
    <div my-widget="Hello World"></div>
    </body>

    var app = angular.module("MyApp", []);

    app.directive("myWidget", function() {
    var linkFunction = function(scope, element, attributes) {
    scope.text = attributes["myWidget"];
    };

    return {
    restrict: "A",
    template: "<p>{{text}}</p>",
    link: linkFunction
    };
    });

```

这将呈现一个文本作为参数传递的段落。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter3/recipe5) 上找到完整的例子。

### 讨论

链接函数可以访问元素及其属性。因此，直接将范围设置为作为属性值传递的文本，并在模板评估中使用它。

但是范围上下文很重要。我们更改的`text`模型可能已经在父范围中定义，并在应用程序的另一部分中使用。为了隔离上下文，从而仅在指令中本地使用它，我们必须返回一个附加的作用域属性:

```js
    return {
    restrict: "A",
    template: "<p>{{text}}</p>",
    link: linkFunction,
    scope: {}
    };

```

在 Angular 中，这称为隔离范围。它通常不从父范围继承，在创建可重用组件时特别有用。

让我们看看将参数传递给指令的另一种方式。这次我们将定义一个 HTML 元素`my-widget2`:

```js
    <my-widget2 text="Hello World"></my-widget2>

    app.directive("myWidget2", function() {
    return {
    restrict: "E",
    template: "<p>{{text}}</p>",
    scope: {
    text: "@text"
    }
    };
    });

```

使用`@text`的范围定义将文本模型绑定到指令的属性。请注意，对父范围`text`的任何更改都会更改局部范围`text`，但不会反过来。

如果您希望在父作用域和局部作用域之间有一个双向绑定，您应该使用`=`相等字符:

```js
    scope: {
    text: "=text"
    }

```

对本地范围的更改也会更改父范围。

另一种选择是使用`&`字符将表达式作为函数传递给指令:

```js
    <my-widget-expr fn="count = count + 1"></my-widget-expr>

    app.directive("myWidgetExpr", function() {
    var linkFunction = function(scope, element, attributes) {
    scope.text = scope.fn({ count: 5 });
    };
    return {
    restrict: "E",
    template: "<p>{{text}}</p>",
    link: linkFunction,
    scope: {
    fn: "&fn"
    }
    };
    });

```

我们将属性`fn`传递给指令，由于局部范围相应地定义了`fn`，因此我们可以在`linkFunction`中调用函数，并将表达式参数作为散列传递。

## 重复渲染指令的 DOM 节点子节点

### 问题

您希望使用指令的子节点作为时间戳内容来重复呈现一个 HTML 片段。

### 溶液

在指令中实现编译函数:

```js
    <repeat-ntimes repeat="10">
    <h1>Header 1</h1>
    <p>This is the paragraph.</p>
    </repeat-n-times>

    var app = angular.module("MyApp", []);

    app.directive("repeatNtimes", function() {
    return {
    restrict: "E",
    compile: function(tElement, attrs) {
    var content = tElement.children();
    for (var i=1; i<attrs.repeat; i++) {
    tElement.append(content.clone());
    }
    }
    };
    });

```

这将渲染标题和段落 10 次。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter3/recipe6) 上找到完整的例子。

### 讨论

该指令重复子节点的频率与`repeat`属性中配置的频率相同。它的工作原理类似于 [ng-repeat](http://docs.angularjs.org/api/ng.directive:ngRepeat) 指令。该实现使用 Angular 的元素方法在循环中追加子节点。

请注意，编译方法只能访问模板元素`tElement`和模板属性。它无法访问范围，因此您也不能使用`$watch`来添加行为。这与链接函数形成对比，链接函数可以访问 DOM 实例(在编译阶段之后)，并且可以访问范围来添加行为。

仅将编译函数用于模板 DOM 操作。每当您想要添加行为时，请使用链接功能。

请注意，您可以同时使用编译和链接函数。在这种情况下，编译函数必须返回链接函数。例如，您希望对点击标题做出反应:

```js
    compile: function(tElement, attrs) {
    var content = tElement.children();
    for (var i=1; i<attrs.repeat; i++) {
    tElement.append(content.clone());
    }

    return function (scope, element, attrs) {
    element.on("click", "h1", function() {
    $(this).css({ "background-color": "red" });
    });
    };
    }

```

单击标题会将背景颜色更改为红色。

## 指令间通信

### 问题

您希望一个指令与另一个指令进行通信，并使用定义良好的接口来增强彼此的行为。

### 溶液

我们实现了一个带有控制器功能的指令`basket`和另外两个需要这个控制器的指令`orange`和`apple,`。我们的示例从用作属性的`apple`和`orange`指令开始:

```js
    <body ng-app="MyApp">
    <basket apple orange>Roll over me and check the console!</basket>
    </body>

```

`basket`指令管理一个可以添加苹果和橘子的数组:

```js
    var app = angular.module("MyApp", []);

    app.directive("basket", function() {
    return {
    restrict: "E",
    controller: function($scope, $element, $attrs) {
    $scope.content = [];

    this.addApple = function() {
    $scope.content.push("apple");
    };

    this.addOrange = function() {
    $scope.content.push("orange");
    };
    },
    link: function(scope, element) {
    element.bind("mouseenter", function() {
    console.log(scope.content);
    });
    }
    };
    });

```

最后是苹果和橘子指令，它们使用篮子的控制器将自己添加到篮子中:

```js
    app.directive("apple", function() {
    return {
    require: "basket",
    link: function(scope, element, attrs, basketCtrl) {
    basketCtrl.addApple();
    }
    };
    });

    app.directive("orange", function() {
    return {
    require: "basket",
    link: function(scope, element, attrs, basketCtrl) {
    basketCtrl.addOrange();
    }
    };
    });

```

如果您将鼠标悬停在呈现的文本上，控制台应该会打印并显示篮子的内容。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter3/recipe7) 上找到完整的例子。

### 讨论

`Basket`是使用控制器功能演示应用编程接口的示例指令，而`apple`和`orange`指令扩充了`basket`指令。它们都用`require`属性定义了对`basket`控制器的依赖。然后`link`功能被注入`basketCtrl`。

注意`basket`指令是如何被定义为一个 HTML 元素的，而`apple`和`orange`指令是如何被定义为 HTML 属性的(指令的默认值)。这展示了由其他指令扩充的可重用组件的典型用例。

现在，可能有其他方法在指令之间来回传递数据；我们已经在前面的菜谱中看到了在指令中使用(隔离的)上下文的不同语义。但是控制器特别棒的是它让你定义了清晰的 API 契约。

## 测试指令

### 问题

您希望用单元测试来测试您的指令。例如，我们将使用选项卡组件指令实现，它可以很容易地在您的 HTML 文档中使用:

```js
    <tabs>
    <pane title="First Tab">First pane.</pane>
    <pane title="Second Tab">Second pane.</pane>
    </tabs>

```

指令实现分为选项卡和窗格指令。让我们从 tab 指令开始:

```js
    app.directive("tabs", function() {
    return {
    restrict: "E",
    transclude: true,
    scope: {},
    controller: function($scope, $element) {
    var panes = $scope.panes = [];

    $scope.select = function(pane) {
    angular.forEach(panes, function(pane) {
    pane.selected = false;
    });
    pane.selected = true;
    console.log("selected pane: ", pane.title);
    };

    this.addPane = function(pane) {
    if (!panes.length) $scope.select(pane);
    panes.push(pane);
    };
    },
    template:
    '<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>',
    replace: true
    };
    });

```

它管理`panes`列表和`panes`的选定状态。模板定义利用选择来改变类，并响应点击事件来改变选择。

`pane`指令依赖于`tabs`指令来添加自身:

```js
    app.directive("pane", function() {
    return {
    require: "^tabs",
    restrict: "E",
    transclude: true,
    scope: {
    title: "@"
    },
    link: function(scope, element, attrs, tabsCtrl) {
    tabsCtrl.addPane(scope);
    },
    template:
    '<div class="tab-pane" ng-class="{active: selected}"' +
    'ng-transclude></div>',
    replace: true
    };
    });

```

### 解

将 angular-seed 与 jasmine 和 jasmine-jquery 结合使用，您可以实现一个单元测试:

```js
    describe('MyApp Tabs', function() {
    var elm, scope;

    beforeEach(module('MyApp'));

    beforeEach(inject(function($rootScope, $compile) {
    elm = angular.element(
    '<div>' +
    '<tabs>' +
    '<pane title="First Tab">' +
    'First content is {{first}}' +
    '</pane>' +
    '<pane title="Second Tab">' +
    'Second content is {{second}}' +
    '</pane>' +
    '</tabs>' +
    '</div>');

    scope = $rootScope;
    $compile(elm)(scope);
    scope.$digest();
    }));

    it('should create clickable titles', function() {
    console.log(elm.find('ul.nav-tabs'));
    var titles = elm.find('ul.nav-tabs li a');

    expect(titles.length).toBe(2);
    expect(titles.eq(0).text()).toBe('First Tab');
    expect(titles.eq(1).text()).toBe('Second Tab');
    });

    it('should set active class on title', function() {
    var titles = elm.find('ul.nav-tabs li');

    expect(titles.eq(0)).toHaveClass('active');
    expect(titles.eq(1)).not.toHaveClass('active');
    });

    it('should change active pane when title clicked', function() {
    var titles = elm.find('ul.nav-tabs li');
    var contents = elm.find('div.tab-content div.tab-pane');

    titles.eq(1).find('a').click();

    expect(titles.eq(0)).not.toHaveClass('active');
    expect(titles.eq(1)).toHaveClass('active');

    expect(contents.eq(0)).not.toHaveClass('active');
    expect(contents.eq(1)).toHaveClass('active');
    });
    });

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter3/recipe8) 上找到完整的例子。

### 讨论

将 jasmine 和 jasmine-jquery 结合起来，您可以得到像`toHaveClass`这样有用的断言和像`click`这样的动作，它们在上面的例子中被广泛使用。

为了准备模板，我们在`beforeEach`函数中使用`$compile`和`$digest`，然后在我们的测试中访问得到的 Angular 元素。

angular-seed 项目略有扩展，将 jquery 和 jasmine-jquery 添加到项目中。

示例代码摘自沃伊塔·纪娜的 GitHub 示例《了不起的 T2》的作者《T3》。