指令是 Angular 中最强大的概念之一,因为它们允许您创建特定于应用程序的自定义 HTML 元素。这允许您开发可重用的组件,这些组件封装复杂的 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-hide
、ng-checked,
或ng-mouseenter.
我鼓励你浏览应用程序编程接口(API)参考并探索 Angular 必须提供的所有指令。
在接下来的食谱中,我们将集中于实现指令。
您希望通过单击鼠标来更改 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 片段呈现为可重用的组件。
实现一个指令,使用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 作为引导。
您的小部件使用指令元素的子节点来创建组合呈现。
将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
属性应该根据您希望子节点附加到的位置来定位。
您希望传递一个配置参数来更改呈现的输出。
使用基于属性的指令并为配置传递属性值。该属性作为参数传递给链接函数:
<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
中调用函数,并将表达式参数作为散列传递。
您希望使用指令的子节点作为时间戳内容来重复呈现一个 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
和另外两个需要这个控制器的指令orange
和apple,
。我们的示例从用作属性的apple
和orange
指令开始:
<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
是使用控制器功能演示应用编程接口的示例指令,而apple
和orange
指令扩充了basket
指令。它们都用require
属性定义了对basket
控制器的依赖。然后link
功能被注入basketCtrl
。
注意basket
指令是如何被定义为一个 HTML 元素的,而apple
和orange
指令是如何被定义为 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》。