Skip to content

Files

Latest commit

1def2f3 · Jan 8, 2022

History

History
609 lines (426 loc) · 17.5 KB

03.md

File metadata and controls

609 lines (426 loc) · 17.5 KB

三、指令

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

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

问题

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

溶液

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

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

你可以在 GitHub 上找到完整的例子。

讨论

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

这只是 Angular 附带的指令的一个例子。还有很多其他的,例如,ng-hideng-checked,ng-mouseenter.我鼓励你浏览应用程序编程接口(API)参考并探索 Angular 必须提供的所有指令。

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

响应用户动作改变 DOM

问题

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

溶液

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

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

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

    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 上找到完整的例子。

讨论

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

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

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

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

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

    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 引用中找到所有支持的方法。

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

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

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

在指令中呈现 HTML 片段

问题

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

溶液

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

    <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 上找到完整的例子。

讨论

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

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

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

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

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

渲染指令的 DOM 节点子节点

问题

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

溶液

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

    <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 上找到完整的例子。

讨论

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

使用 HTML 属性传递配置参数

问题

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

溶液

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

    <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 上找到完整的例子。

讨论

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

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

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

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

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

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

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

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

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

    scope: {
    text: "=text"
    }

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

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

    <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 片段。

溶液

在指令中实现编译函数:

    <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 上找到完整的例子。

讨论

该指令重复子节点的频率与repeat属性中配置的频率相同。它的工作原理类似于 ng-repeat 指令。该实现使用 Angular 的元素方法在循环中追加子节点。

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

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

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

    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和另外两个需要这个控制器的指令orangeapple,。我们的示例从用作属性的appleorange指令开始:

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

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

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

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

    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 上找到完整的例子。

讨论

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

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

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

测试指令

问题

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

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

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

    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指令来添加自身:

    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 结合使用,您可以实现一个单元测试:

    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 上找到完整的例子。

讨论

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

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

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

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