指令是AngularJS最强大的组件之一,可帮助您扩展基本HTML元素/属性并创建可重用和可测试的代码。 在本教程中,我将向您展示如何将AngularJS指令与实际最佳实践结合使用。
我在这里指的是指令 在教程中主要是自定义指令。 我不会尝试教您如何使用内置指令(例如ng-repeat
, ng-show
等)。我将向您展示如何使用自定义指令来创建自己的组件。
大纲
- 简单指令
- 指令限制
- 孤立范围
- 指令范围
- 指令继承
- 指令调试
- 指令单元测试
- 指令范围测试
- 结论
1.简单指令
假设您有一个关于图书的电子商务应用程序,并且正在多个区域显示特定的图书详细信息,例如评论,用户个人资料页面,文章等。您的图书详细信息小部件可能如下所示:
在此小部件中,有书籍图像,标题,描述,评论和评分。 收集这些信息并放入特定的dom元素可能在您想使用的每个地方都很难做到。 让我们通过使用AngularJS指令来对此视图进行窗口化。
angular.module('masteringAngularJsDirectives', [])
.directive('book', function() {
return {
restrict: 'E',
scope: {
data: '='
},
templateUrl: 'templates/book-widget.html'
}
})
上面的示例中已使用指令函数首先创建指令。 该指令的名称是book
。 该指令返回一个对象,让我们来谈谈这个对象。 restrict
用于定义指令类型,它可以是A
(A ttribute), C
( C lass), E
( E lement)和 M
(共中号 MENT)。 您可以在下面分别查看每个的用法。
类型 | 用法 |
---|---|
一个 | <div 书 > </ div> |
C | <div class =“ book ”> </ div> |
Ë | < book data =“ book_data”> </ book > |
中号 | <!-directive: 书 -> |
scope
用于管理指令范围。 在上述情况下,通过使用"="
将图书数据传输到指令模板 范围类型。 我将在以下各节中详细讨论范围。 templateUrl
用于调用视图,以便通过使用传输到指令范围的数据来呈现特定内容。 您也可以使用template
并直接提供HTML代码,如下所示:
.....
template: '<div>Book Info</div>'
.....
在我们的例子中,我们有一个复杂HTML结构,这就是为什么我选择templateUrl
选项。
2.指令限制
指令在AngularJS项目JavaScript文件中定义,并在HTML页面中使用。 可以在HTML页面中使用AngularJS指令,如下所示:
A(属性)
在这种用法中,指令名称在标准HTML元素内使用。 假设您在电子商务应用程序中有一个基于角色的菜单。 该菜单是根据您当前的角色而形成的。 您可以定义一个指令来决定是否显示当前菜单。 您HTML菜单可能如下所示:
<ul>
<li>Home</li>
<li>Latest News</li>
<li restricted>User Administration</li>
<li restricted>Campaign Management</li>
</ul>
指令如下:
app.directive("restricted", function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// Some auth check function
var isAuthorized = checkAuthorization();
if (!isAuthorized) {
element.css('display', 'none');
}
}
}
})
如果您使用restricted
菜单元素中的指令作为属性,您可以对每个菜单进行访问级别检查。 如果当前用户未被授权,则不会显示该特定菜单。
那么,什么是link
有功能吗? 简而言之,链接功能是可用于执行指令特定操作的功能。 该指令不仅通过提供一些输入来呈现一些HTML代码。 您还可以将函数绑定到指令元素,调用服务并更新指令值,如果指令值为E
,则获取指令属性。 类型指令等
C(班级)
您可以在HTML元素类中使用指令名称。 假设您将上述指令用作C
,则可以将指令restrict
更新为C
并如下使用它:
<ul>
<li>Home</li>
<li>Latest News</li>
<li class="nav restricted">User Administration</li>
<li class="nav active restricted">Campaign Management</li>
</ul>
每个元素已经有一个用于样式的类,并且当添加restricted
类时,它实际上是一个指令。
E(元素)
您无需在HTML元素内使用指令。 您可以使用带有E
限制的AngularJS指令来创建自己的元素。 假设您的应用程序中有一个用户小部件,用于显示username
, avatar
和reputation
在您的应用程序中的几个地方。 您可能要使用这样的指令:
app.directive("user", function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.username = attrs.username;
scope.avatar = attrs.avatar;
scope.reputation = attrs.reputation;
},
template: '<div>Username: {{username}}, Avatar: {{avatar}}, Reputation: {{reputation}}</div>'
}
})
HTML代码为:
<user username="huseyinbabal" avatar="https://www.gravatar.com/avatar/ef36a722788f5d852e2635113b2b6b84?s=128&d=identicon&r=PG" reputation="8012"></user>
在上面的示例中,创建了一个自定义元素,并提供了一些属性,如username
, avatar
和reputation
。 我想提请注意链接功能主体。 元素属性分配给指令范围。 链接功能的第一个参数是当前指令的范围。 指令的第三个参数是指令的属性对象,这意味着您可以使用attrs.attr_name
从自定义指令读取任何属性。 将属性值分配给作用域,以便在模板内部使用它们。
实际上,您可以以更短的方式执行此操作,我将在稍后讨论。 本示例用于理解用法背后的主要思想。
M(矩)
这种用法不是很常见,但是我将展示如何使用它。 假设您需要一个注释表单,以便您的应用程序可以在许多地方使用。 您可以使用以下指令来做到这一点:
app.directive("comment", function() {
return {
restrict: 'M',
template: '<textarea class="comment"></textarea>'
}
})
在HTML元素中:
<!-- directive:comment -->
3.隔离范围
每个指令都有其自己的范围,但是您需要注意与指令声明的数据绑定。 假设您正在实施basket
电子商务应用程序的一部分。 在购物篮页面上,您之前已经在这里添加了项目。 每个项目都有其数量字段,用于选择要购买的项目数量,如下所示:
这是指令声明:
app.directive("item", function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.name = attrs.name;
},
template: '<div><strong>Name:</strong> {{name}} <strong>Select Amount:</strong> <select name="count" ng-model="count"><option value="1">1</option><option value="2">2</option></select> <strong>Selected Amount:</strong> {{count}}</div>'
}
})
并且为了显示HTML中的三个项目:
<item name="Item-1"></item>
<item name="Item-2"></item>
<item name="Item-3"></item>
这里的问题是,每当您选择所需项目的金额时,项目的所有金额部分都会被更新。 为什么? 因为存在带有名称count
的双向数据绑定,但是作用域不是孤立的。 为了隔离范围,只需添加scope: {}
返回部分的指令属性:
app.directive("item", function() {
return {
restrict: 'E',
scope: {},
link: function(scope, element, attrs) {
scope.name = attrs.name;
},
template: '<div><strong>Name:</strong> {{name}} <strong>Select Amount:</strong> <select name="count" ng-model="count"><option value="1">1</option><option value="2">2</option></select> <strong>Selected Amount:</strong> {{count}}</div>'
}
})
这导致您的指令具有自己的隔离范围,因此双向数据绑定将在此指令内单独进行。 我还会提到scope
稍后属性。
4.指令范围
该指令的主要优点是它是一个易于使用的可重用组件,您甚至可以为该指令提供一些其他属性 。 但是,如何将附加值,绑定或表达式传递给指令,以便在指令内部使用数据?
“ @”作用域:这种类型的作用域用于将值传递给指令作用域。 假设您要为通知消息创建小部件:
app.controller("MessageCtrl", function() {
$scope.message = "Product created!";
})
app.directive("notification", function() {
return {
restrict: 'E',
scope: {
message: '@'
},
template: '<div class="alert">{{message}}</div>'
}
});
您可以使用:
<notification message="{{message}}"></notification>
在此示例中,仅将消息值分配给指令范围。 呈现HTML内容将为:
<div class="alert">Product created!</div>
“ =”范围:在此范围类型中,将传递范围变量而不是值,这意味着我们将不传递{{message}}
,而是传递message
。 此功能背后的原因是在指令与页面元素或控制器之间构造双向数据绑定。 让我们来看看它的作用。
.directive("bookComment", function() {
return {
restrict: 'E',
scope: {
text: '='
},
template: '<input type="text" ng-model="text"/>'
}
})
在此指令中,我们试图创建一个小部件来显示注释文本输入,以对特定书籍进行注释。 如您所见,此伪指令需要一个属性text
来构造页面上其他元素之间的双向数据绑定。 您可以在页面上使用它:
<span>This is the textbox on the directive</span>
<book-comment text="commentText"></book-comment>
这只会在页面上显示一个文本框,因此让我们添加更多内容与该指令交互:
<span>This is the textbox on the page</span>
<input type="text" ng-model="commentText"/>
<br/>
<span>This is the textbox on the directive</span>
<book-comment text="commentText"></book-comment>
每当您在第一个文本框中键入内容时,也会在第二个文本框中键入内容。 反之亦然。 在指令中,我们传递了范围变量commentText
而不是值,并且此变量是对第一个文本框的数据绑定引用。
“&”范围:我们能够传递值,并引用指令。 在这种作用域类型中,我们将研究如何将表达式传递给指令。 在实际情况下,您可能需要将特定的函数(表达式)传递给指令,以防止耦合。 有时,指令不需要对表达式背后的概念有太多了解。 例如,一条指令会为您喜欢这本书,但它不知道该怎么做。 为此,您可以遵循以下结构:
.directive("likeBook", function() {
return {
restrict: 'E',
scope: {
like: '&'
},
template: '<input type="button" ng-click="like()" value="Like"/>'
}
})
在此指令中,表达式将通过like
属性传递给指令按钮。 让我们在控制器中定义一个函数,并将其传递给HTML内部的指令。
$scope.likeFunction = function() {
alert("I like the book!")
}
这将在控制器内部,并且模板将是:
<like-book like="likeFunction()"></like-book>
likeFunction()
来自控制器,并传递给指令。 如果要将参数传递给likeFunction()
怎么办? 例如,您可能需要将等级值传递给likeFunction()
。 这非常简单:只需在控制器内部的函数中添加参数,然后在指令中添加输入元素,以要求用户进行起始计数。 您可以如下所示进行操作:
.directive("likeBook", function() {
return {
restrict: 'E',
scope: {
like: '&'
},
template: '<input type="text" ng-model="starCount" placeholder="Enter rate count here"/><br/>' +
'<input type="button" ng-click="like({star: starCount})" value="Like"/>'
}
})
$scope.likeFunction = function(star) {
alert("I like the book!, and gave " + star + " star.")
}
<like-book like="likeFunction(star)"></like-book>
如您所见,文本框来自指令。 文本框的值绑定到函数参数,例如like({star: starCount})
。 star
用于控制器功能, starCount
用于文本框值绑定。
5.指令继承
有时,您可能具有多个指令中存在的功能。 可以将它们放在父指令中,以便由子指令继承。
让我给你一个真实的例子。 您希望每当客户将鼠标光标移到特定书籍的顶部时发送统计数据。 您可以为book指令实现鼠标单击事件,但是如果另一个指令将使用该事件呢? 在这种情况下,可以使用如下指令的继承:
app.directive('mouseClicked', function() {
return {
restrict: 'E',
scope: {},
controller: "MouseClickedCtrl as mouseClicked"
}
})
这是要由子指令继承的父指令。 如您所见,使用“ as”指令存在指令的控制器属性。 让我们也定义这个控制器:
app.controller('MouseClickedCtrl', function($element) {
var mouseClicked = this;
mouseClicked.bookType = null;
mouseClicked.setBookType = function(type) {
mouseClicked.bookType = type
};
$element.bind("click", function() {
alert("Typeof book: " + mouseClicked.bookType + " sent for statistical analysis!");
})
})
在此控制器中,我们仅使用子指令设置变量bookType
的控制器实例。 每当您单击一本书或杂志时,元素的类型都会发送到后端服务(我使用了警报功能只是为了显示数据)。 子指令将如何使用该指令?
app.directive('ebook', function() {
return {
require: "mouseClicked",
link: function(scope, element, attrs, mouseClickedCtrl) {
mouseClickedCtrl.setBookType("EBOOK");
}
}
})
.directive('magazine', function() {
return {
require: "mouseClicked",
link: function(scope, element, attrs, mouseClickedCtrl) {
mouseClickedCtrl.setBookType("MAGAZINE");
}
}
})
如您所见,子指令使用require
关键字来使用父指令。 还有一点很重要,就是子指令中链接函数的第四个参数。 此参数引用父指令的controller属性,这意味着子指令可以在控制器内部使用控制器函数setBookType
。 如果当前元素是电子书,则可以使用第一个指令,如果它是杂志,则可以使用第二个指令:
<a><mouse-clicked ebook>Game of thrones (click me)</mouse-clicked></a><br/>
<a><mouse-clicked magazine>PC World (click me)</mouse-clicked></a>
子指令类似于父指令的属性。 我们通过将每个节放在父指令中来消除了每个子指令对mouse-click事件的使用。
6.指令调试
当您在模板中使用指令时,在页面上看到的是指令的编译版本。 有时,您希望查看实际的指令用法以进行调试。 为了查看当前部分的未编译版本,可以使用ng-non-bindable
。 例如,假设您有一个可打印最受欢迎书籍的小部件,以下是该代码:
<ul>
<li ng-repeat="book in books">{{book}}</li>
</ul>
本书的范围变量来自控制器,其输出如下:
如果您想了解此编译输出背后的指令用法,则可以使用以下版本的代码:
<ul ng-non-bindable="">
<li ng-repeat="book in books">{{book}}</li>
</ul>
这次的输出将如下所示:
到现在为止还很酷,但是如果我们想同时看到小部件的未编译版本和已编译版本,该怎么办? 现在该编写一个可以执行高级调试操作的自定义指令了。
app.directive('customDebug', function($compile) {
return {
terminal: true,
link: function(scope, element) {
var currentElement = element.clone();
currentElement.removeAttr("custom-debug");
var newElement = $compile(currentElement)(scope);
element.attr("style", "border: 1px solid red");
element.after(newElement);
}
}
})
在此伪指令中,我们将克隆处于调试模式的元素,以便在进行某些操作后不会更改它。 克隆后,删除custom-debug
为了不充当调试模式,然后使用已经注入到指令中的$complile
进行编译。 我们为调试模式元素提供了一种样式,以强调已调试的元素。 最终结果如下:
通过使用这种调试指令来检测项目中任何错误的根本原因,可以节省开发时间。
7.指令单元测试
如您所知,单元测试是开发中非常重要的一部分,它可以完全控制您编写的代码并防止潜在的错误。 我不会深入探讨单元测试,但是会为您提供有关如何以几种方式测试指令的线索。
我将使用Jasmine进行单元测试,使用Karma进行单元测试。 为了使用Karma,只需通过运行npm install -g karma karma-cli
全局npm install -g karma karma-cli
(您需要在计算机上安装Node.js和npm)。 安装后,打开命令行,转到项目根文件夹,然后键入karma init
。 它会询问您以下几个问题,以设置您的测试要求。
我正在使用Webstorm进行开发,如果您还使用Webstorm,则只需右键单击karma.conf.js并选择Run karma.conf.js。 这将执行karma conf中配置的所有测试。 您也可以使用项目根文件夹中的karma start
命令行运行测试。 这就是环境设置的全部内容,因此让我们切换到测试部分。
假设我们要测试book指令。 将标题传递给指令时,应将其编译为书籍详细信息视图。 因此,让我们开始吧。
describe("Book Tests", function() {
var element;
var scope;
beforeEach(module("masteringAngularJsDirectives"))
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope;
element = angular.element("<booktest title='test'></booktest>");
$compile(element)($rootScope)
scope.$digest()
}));
it("directive should be successfully compiled", function() {
expect(element.html()).toBe("test")
})
});
在上述测试中,我们正在测试一个名为booktest
的新指令。 该指令采用参数title
并使用此标题创建一个div。 在测试中,在每个测试部分之前,我们都将模块称为masteringAngularJsDirectives
第一。 然后,我们正在生成一个名为booktest
的指令。 在每个测试步骤中,将测试指令输出。 该测试仅用于值检查。
8.指令范围测试
在本节中,我们将测试指令booktest
的范围。 此伪指令在页面上生成一个书籍详细信息视图,当您单击此详细信息部分时,一个范围变量称为已viewed
将设置为true
。 在我们的测试中,我们将检查是否viewed
触发click事件时将其设置为true。 该指令是:
.directive('booktest', function() {
return {
restrict: 'E',
scope: {
title: '@'
},
replace: true,
template: '<div>{{title}}</div>',
link: function(scope, element, attrs) {
element.bind("click", function() {
console.log("book viewed!");
scope.viewed = true;
});
}
}
})
为了在指令内的AngularJS中将事件设置为元素,可以使用link
属性。 在此属性内,您具有直接绑定到click事件的当前元素。 为了测试此指令,可以使用以下命令:
describe("Book Tests", function() {
var element;
var scope;
beforeEach(module("masteringAngularJsDirectives"))
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope;
element = angular.element("<booktest title='test'></booktest>");
$compile(element)($rootScope)
scope.$digest()
}));
it("scope liked should be true when book liked", function() {
element.triggerHandler("click");
expect(element.isolateScope().viewed).toBe(true);
});
});
在测试部分,使用element.triggerHandler("click")
触发click事件。 触发点击事件时,需要将查看的变量设置为true
。 通过使用expect(element.isolateScope().viewed).toBe(true)
来声明该值。
9.结论
为了开发模块化和可测试的Web项目,AngularJS是最好的工具。 指令是AngularJS的最佳组件之一,这意味着您对AngularJS指令了解得越多,您可以开发的模块和可测试项目就越多。
在本教程中,我试图向您展示有关指令的现实最佳实践,并牢记您需要做大量练习才能理解指令背后的逻辑。 我希望本文能帮助您更好地了解AngularJS指令。
翻译自: https://code.tutsplus.com/tutorials/mastering-angularjs-directives--cms-22511