发表于: 2017-06-02 21:26:02
1 1113
今天完成的事情:
看了创建自定义函数和实现链接函数以及打破对数据属性的依赖。
做小课堂。
明天计划的事情:
做小课堂,看完指令这一章。
遇到的问题:
暂无
收获:
在上面的13.10中隐藏着一个常见的陷阱(但不是bug),几乎是所有初次使用控制器继承的人都会遇到的问题。看看这个问题的话,在浏览器中打开文件并轮流单机“Reverse”按钮(单击的顺序没关系)。
"Reverse"按钮调用了对dataValue属性进行操作的reverseText行为。该行为和数据是在父控制器上定义的,然后被子控制器继承,所有所有三个输入框元素的内容会一起改变。
现在修改关联到第二个自控制器的输入框元素中的内容。输入什么无关紧要,只需要修改过该文本即可。可以再次轮流所有三个"Reverse"按钮,你将会看到另外一种行为。所有三个按钮支队前两个输入框元素起作用,而被编辑过的那个输入框元素毫无变化。
先说解决方案:
如果将新版本的contrlllers.html文件载入到浏览器中,你将会看到所有的按钮都可以影响所有的输入框元素,而且编辑输入框元素的内容也不会影响后续的变化。
要理解这是怎么回事,我们需要看看AngularJS对作用域上的数据值的继承的处理方式,以及这是如何收ng-model指令影响的。
当读取一个直接在作用域上定义的属性的值时,AngularJS会检查在这个控制器的作用域上是否有一个局部属性,如果没有,就会沿着作用域层次结构向上查找是否有一个被继承的属性。然而,当时用ng-model指令来修改这样一个属性时,AngularJS 会检查当前作用域是否有这样的一个名称的属性,如果没有,就会假设你想隐式定义一个这样的属性。就会影响"Reverse"按钮的工作的原因是因为现在会有两个dataValue属性-一个是被顶层控制器所定义的,另一个是被编辑的子控制器所定义的。reverseText行为是在顶层控制器中定义的,只对顶层作用域中定义的dataValue属性起作用,而不会改变子作用域中的dataValue属性。
而如果在作用域上定义一个对象然后在对象上定义数据属性,这一切却不会发生。这是因为JavaScript对继承的实现是基于所谓的"原型继承"-这是一个相当枯燥且容易让人混淆的话题,因此这里不展开。
重要的知识点是,像这样直接在作用域上定义属性:
意味着使用ng-model指令时将会创建局部变量,并使用一个对象作为中介着,类似如下:
这将确保ng-model会对在父作用域上定义的数据值进行更新。这可不是bug(全面说错了)。这是一个专门设计的特性,以允许你自己决定控制器及其作用域如何工作,你还可以在同一个作用域中混合使用这两种技术。如果你想数据值在开始时是被共享的但在修改时会被复制一份,就直接在作用域上定义数据属性。如果想确保始终只有一份数据值,就通过一个对象来定义数据属性。
tip:我用来演示继承关系的控制器行为都是直接在其作用域上定义的值上直接进行操作的。这样做是为了将继承所带来的问题演示得更明显些,但是AngularJS开发中的习惯是使用接受参数的行为。这并不改变继承的工作方式-或说继承所造成的易混淆之处-因为在查找值时,无论是从行为中直接访问还是从参数中传递,AngularJS都市遵循相同的步骤顺序的。
使用无作用域的控制器
如果作用域看起来不必要的复杂,而且你的应用程序并未从继承中得到好处,也不需要在控制器之间进行通信,你可以无作用域的控制器。这些控制器可以在根本不需要使用作用域的情况下向视图提供数据和行为。取而代之的是一个提供给视图的代表控制器的特殊变量,如下
在本例中的控制器并未声明对$scope的依赖,而是通过JavaScript的关键字this定义了自己的数据值和行为,如下:
当应用无作用域的控制器时,ng-controller指令的表达式格式会有所不同,需要制定一个代表控制器的的变量名,将在视图中访问他:
表达式的格式形如:<要应用的控制器> as <变量名>,本例中对div元素应用了simpleCtrl控制器,并创建了一个名为ctrl的变量。然后在视图中使用ctrl变量访问数据和行为,类似这样
无作用域的控制器避免了作用域的复杂性,但这是一个AngularJS中相对较新的新增特性,还未得到广泛应用。我的建议是,多花时间在掌握作用域的操作方式上,这样你可以充分利用AngularJS所提供的全部特性-不仅仅是只在控制器上工作,也需要创建自定义指令。
显式地更新作用域
在大多数情况下,AngularJS在自动更新作用域方面表现得相当好,但是有时需要读该过程实现更直接的操作,例如将AngularJS于另外的JavaScript框架集成起来。你不可能总是在一个应用中全是用AngularJS,特别是将新功能集成到一个已有的产品或服务中,而该产品或服务已经使用了一个不同的客户端框架时。
可以通过在作用域对象上定义的三种方法将AngularJS和其他框架集成起来。下表中介绍的这些方法允许你注册响应作用域上变化的处理函数,以及从AngularJS代码之外向作用域注入变化。
我打算使用jQuery UI来掩饰这些方法是如何工作的。UI是一个来自jQuery团队的UI工具包,他提供了在jQuery基础上打造的一系列优秀的小部件,并且能够在许多各种不同的浏览器上工作。
tip:你也可以向$apply方法传递函数而不是表达式,在创建自定义指令时尤为有用,并且允许你在响应用户交互时使用指令所管理的元素自己定义对作用域的更新方法。
前面演示过如何使用AngularJS提供的内置指令,包括可以处理你单向和双向数据绑定的指令(ng-bind和ng-model)、可以从数据生成内容的指令(例如ng-repeat和ng-swtich)、操作HTML元素的指令(例如ng-class和ng-if),以及相应用户交互的指令(比如ng-click和ng-change),还有可用来替换标准HTML表单的指令,用于启用数据校验以及执行一些常用任务。
AngularJS拥有一系列全面的指令,可用于处理Web应用程序中大多数常见的情景,但是,在内置指令不能按照你需要的方式工作时,当然也可以创建你自己的指令。
tip:正如你将学到的,编写自定义指令需要依赖之前介绍过的功能,特别是作用域,这也是为什么再返回关于指令的话题之前需要先涵盖其他话题。
创建自定义指令
我打算从一个简单的例子开始,演示如何创建自定义指令,概述基本特性并为本章中后面的例子做好准备。初始目标是创建并应用一个指令,该指令能够生成一个ul元素,该元素包含products数组中的各个对象生成的li元素。
定义指令
使用module.dirctive方法来创建指令,参数是新指令的名称和一个用于创建指令的共厂函数。
<script>
angular.module("exampleApp", [] )
.directive("unorderedList", function() {
return function(scope, element, attrs) {
//implementation code will go here
}
})
.controller("defalutCtrl", function($scope) {
$scope.products = [
{ name: "Apples", category: "Fruit", price: 1.20, expiry: 10 },
{ name: "Bananas", category: "Fruit", price: 2.42, expiry: 7 },
{ name: "Pears", category: "Fruit", price: 2.02, expiry: 6 }
];
});
</script>
tip:在本清单中是在控制器的定义之前定义指令的额,但这并不是必需的,在较大的项目中一般是在一个或多个独立的文件中定义指令。
传给directive方法的第一个参数设置新指令的的名称为unorderedList。注意这里使用了标准JavaScript大小写习惯,也就是说unordered的u是小写,而List中的L是大写。AngularJS在遇到混合大小写的名称时有点特殊。如下
<body ng-controller="defalutCtrl">
<div class="panel panel-default">
<div>
<h3>Products</h3>
</div>
<div>
<div unordered-list="profucts"></div>
</div>
</div>
</body>
这里将该指令用作div元素的一个属性,但请注意属性名和传给directive方法的参数有所不同,是unordered-list而不是unorderedList。传给方法的参数中每个大写字母被认为是属性名中的一个独立的词,而每个词之间是以一个连字符分隔的。
(tip:本例中将指令用作一个属性,但是在后面将演示如何创建和应用可用作HTML元素的指令,可用作class属性值的指令,以及甚至可用作注释的指令。就是EACM)
这里将该属性值设置为待展示的数组名,在本例中即是products。指令是专门用于在应用程序内或应用程序之间复用的,所以要避免产生硬链接的依赖关系,包括引用被特定控制器所创建的数据。
实现链接函数
指令中的worker函数被称为链接函数,它提供了将指令与HTML文档和作用域数据相连接的方法(另一种可以与指令相关联的函数,称为编译函数)
当AngularJS建立指令的每个实例时,链接函数便被调用并接受三个参数:指令被应用到的视图的作用域,指令被应用到的HTML元素,以及HTML元素的属性。惯例是使用scope、element和attrs这些参数来定义链接函数。
tip:scope、element和attrs参数只是普通的JavaScript参数,而不是通过依赖注入提供的。也就意味着被传入链接函数的对象的顺序应是固定的。
从作用域获取数据
实现自定义指令需要做的第一步是从作用域中获取要显示的数据。与AngularJS控制器不同,指令并不声明对$scope服务的依赖,取而代之的是,传入的是指令被应用到的视图的控制器所创建的作用域。这很重要,因为他允许单个指令在一个应用程序中被使用多次,而每个程序可能是在作用域层次结构上的不同作用域上工作的
例子中将自定义指令用作div元素的一个属性,并且使用属性值指定待处理的作用域中的数组名:类似这样
<div unordered-list="profucts"></div>
要从作用域中获取数据,需要先得到该属性值(如products)。链接函数的第三个参数是一个按照名字索引的属性集合。获取使用指令的属性名并没有什么特殊的,如下
angular.module("exampleApp", [] )
.directive("unorderedList", function() {
return function(scope, element, attrs) {
var data = scope[attrs["unorderedList"]];
if(angular.isArray(data)) {
for (var i = 0; i < data.length; i++) {
console.log("Item: " + data[i].name) ;
}
}
}
})
从attrs集合中使用"unorder"作为key获取相关的值,然年传给scope对象来获取数据类似这样
var data = scope[attrs["unorderedList"]];
tip:注意这里使用了"unorderedList"来获取unordered-list属性的值。AngularJS在两种命名格式之间会自动进行映射。"unorderedList"这样的形式是标准化名称的一个例子,能够被指令以多种不同形式应用于html中
一获取到数据,就是用angular.isArray方法检查该数据是否是确实为数据,并使用for循环将每个对象的name属性写到控制台中。(在实际项目中这可能是一个蹩脚的设计,因为这假定了指定要处理的所有对象都拥有一个name属性,妨碍了重用,后面介绍"计算表达式"来演示如何能够使之更灵活。)
生成html元素
下一步是从数据对象中生成所需的元素。AngularJS包含了一个裁剪过的版本的jQuery,称为jqLite。它不具有jQuery的所有特性,但是却拥有足够的与指令相工作的功能。
<!DOCTYPE html>
<html ng-app='exampleApp'>
<head>
<meta charset="UTF-8">
<title>清单15.5</title>
<link rel="stylesheet" type="text/css" href="bootstrap.css">
<link rel="stylesheet" type="text/css" href="bootstrap-theme.css">
<script src='angular.js'></script>
<script>
angular.module("exampleApp", [] )
.directive("unorderedList", function() {
return function(scope, element, attrs) {
var data = scope[attrs["unorderedList"]];
if(angular.isArray(data)) {
var listElem = angular.element("<ul>");
element.append(listElem);
for (var i = 0; i < data.length; i++) {
listElem.append(angular.element("<li>").text(data[i].name)) ;
}
}
}
})
.controller("defalutCtrl", function($scope) {
$scope.products = [
{ name: "Apples", category: "Fruit", price: 1.20, expiry: 10 },
{ name: "Bananas", category: "Fruit", price: 2.42, expiry: 7 },
{ name: "Pears", category: "Fruit", price: 2.02, expiry: 6 }
];
});
</script>
</head>
<body ng-controller="defalutCtrl">
<div class="panel panel-default">
<div>
<h3>Products</h3>
</div>
<div>
<div unordered-list="products"></div>
</div>
</div>
</body>
</html>
jqList的功能通过传给链接函数的element参数暴露出来。首先调用了angular.element方法创建一个新元素并且在element参数上使用append方法向文档中插入这个新元素,类似这样:
var listElem = angular.element("<ul>");
element.append(listElem);
大多数jqLite方法返回的结果是拥有访问jqLite各种功能的另一个对象,就像完整的jQuery库中的方法返回jQuery对象那样。AngularJ不会暴露浏览器所提供的DOM API,任何时候如果想对元素进行操作,都会期望接受一个jqLite对象。后面会将jqLite方法所返回的结构称为jqLite对象。
如果没有jqLite对象却需要一个(例如要创建一个新元素时),就可以使用angular.element方法类似这样
angular.element("<li>").text(data[i].name)
两种方法都返回jqLite对象,可以用于调用其他的jqLite方法,也就是被称为方法链的技术。上面当创建li元素并调用text方法设置其内容时,包含了一个方法链的例子,就是以后面的.text(data[i].name)
能够提供对方法链的支持的库被称作可提供fluent API的,Jquery(jqLite就是起源于jQuery)是最广泛应用的fluent API之一。
jqLite产生的附加结果是我的自定义指令将会给被应用到的数据的每个对象创建一个内嵌的li元素。结果如下
打破对数据属性的依赖
我的自定义指令已经可以工作了,但是他存在对用于生成列表的数组对象的依赖:他假定这些对象都有一个name属性。这种依赖将指令和具体的数据对象集合绑定起来了,也就意味着无法在程序的别处或者在其他程序中使用。有几种处理这种情形的方式,下面介绍
1.添加一个支持属性
第一种办法是最简单的,需要定义一个属性,用来指定哪个属性的值将会被显示在li项目中。这很好做,因为链接函数被传入了指令所应用到的元素上定义的所有属性的集合。看下面list-property
<!DOCTYPE html>
<html ng-app='exampleApp'>
<head>
<meta charset="UTF-8">
<title>清单15.6</title>
<link rel="stylesheet" type="text/css" href="bootstrap.css">
<link rel="stylesheet" type="text/css" href="bootstrap-theme.css">
<script src='angular.js'></script>
<script>
angular.module("exampleApp", [] )
.directive("unorderedList", function() {
return function(scope, element, attrs) {
var data = scope[attrs["unorderedList"]];
var propertyName = attrs["listProperty"];
if(angular.isArray(data)) {
var listElem = angular.element("<ul>");
element.append(listElem);
for (var i = 0; i < data.length; i++) {
listElem.append(angular.element("<li>").text(data[i][propertyName])) ;
}
}
}
})
.controller("defalutCtrl", function($scope) {
$scope.products = [
{ name: "Apples", category: "Fruit", price: 1.20, expiry: 10 },
{ name: "Bananas", category: "Fruit", price: 2.42, expiry: 7 },
{ name: "Pears", category: "Fruit", price: 2.02, expiry: 6 }
];
});
</script>
</head>
<body ng-controller="defalutCtrl">
<div class="panel panel-default">
<div>
<h3>Products</h3>
</div>
<div>
<div unordered-list="products" list-property="name"></div>
</div>
</div>
</body>
</html>
本例中通过传给链接函数的attrs参数获得list-property属性的值,使用key的名字为
listProperty。再说一遍,AngularJS已经将属性名规范化了。然后使用listProperty属性的值从每个数据对象中获取一个值,类似这样:
listElem.append(angular.element("<li>").text(data[i][propertyName]));
tip:如果属性名是以data-为前缀的,AngularJS会生成传给链接函数的属性集合时移除这一前缀。也就是说,例如,当属性名被规范化并传给链接函数时,属性名data-list-property和list-property都会被表示为listProperty
2计算表达式
另外增加一个属性是有帮助的,当时仍然存在一些问题。如下面例子要对显示的属性应用了一个过滤器。
<body ng-controller="defalutCtrl">
<div class="panel panel-default">
<div>
<h3>Products</h3>
</div>
<div>
<div unordered-list="products" list-property="price | currency"></div>
</div>
</div>
</body>
这以修改破坏了我的自定义指令,因为我是从属性中读出值并将该值用作要显示在每个生成的li元素中的属性名。这个问题的解决方案是让作用于将属性值当做一个表达式来进行计算,通过scope.$eval方法可以做到这点,传给该方法的是要计算的表达式和需要用于执行该计算的人以本地数据。如下:
.directive("unorderedList", function() {
return function(scope, element, attrs) {
var data = scope[attrs["unorderedList"]];
var propertyExpression = attrs["listProperty"];
if(angular.isArray(data)) {
var listElem = angular.element("<ul>");
element.append(listElem);
for (var i = 0; i < data.length; i++) {
listElem.append(angular.element("<li>").text(scope.$eval(propertyExpression, data[i]) )) ;
}
}
}
})
获取到listProperty属性的值后,就得到了一个需要当做表达式进行计算的字符串。在创建li元素时,在传给链接函数的scope参数上调用$eval方法,并传入表达式和当前数据对象,用作需要计算的表达式的属性的来源。AngularJS来负责做完其余的事。下乳可以看到其效果,说明了li元素是如何将每个数据对象的price属性包含进去的,并呗currency过滤器格式化的。
处理数据变化
要为指令进行介绍的下一个特性是响应作用域中数据变化的能力。目前在HTML页被AngularJS处理时li元素的内容就已经被设置了,并且在底层数据值变化时无法自动更新。在下面例子中,可以看到用于改变product对象的price属性值。
(tip:我打算分解这一处理变化的过程,因为我想演示关于指令在AngularJS和JavaScript之间经常发生的一个问题,并解释解决方案。)
<!DOCTYPE html>
<html ng-app="exampleApp">
<head>
<meta charset="UTF-8">
<title>清单15.9</title>
<link rel="stylesheet" type="text/css" href="bootstrap.css">
<link rel="stylesheet" type="text/css" href="bootstrap-theme.css">
<script src='angular.js'></script>
<script>
angular.module("exampleApp", [] )
.directive("unorderedList", function() {
return function(scope, element, attrs) {
var data = scope[attrs["unorderedList"]];
var propertyExpression = attrs["listProperty"];
if(angular.isArray(data)) {
var listElem = angular.element("<ul>");
element.append(listElem);
for (var i = 0; i < data.length; i++) {
listElem.append(angular.element("<li>").text(scope.$eval(propertyExpression, data[i]) )) ;
}
}
}
})
.controller("defaultCtrl", function($scope) {
$scope.products = [
{ name: "Apples", category: "Fruit", price: 1.20, expiry: 10 },
{ name: "Bananas", category: "Fruit", price: 2.42, expiry: 7 },
{ name: "Pears", category: "Fruit", price: 2.02, expiry: 6 }
];
$scope.incrementPrices = function() {
for (var i = 0; i < $scope.products.length; i++) {
$scope.products[i].price++;
console.log($scope.products[i].price);
}
}
})
</script>
</head>
<body ng-controller="defaultCtrl">
<div class="panel panel-default">
<div>
<h3>Products</h3>
</div>
<div>
<button class="btn btn-primary" ng-click="incrementPrices()">Change Prices</button>
</div>
<div>
<div unordered-list="products" list-property="price | currency"></div>
</div>
</div>
</body>
</html>
本例中添加了一个按钮并使用了ng-click指令,以便控制器中的incrementPrices行为能被调用到。这个行为相当简单,使用了一个for循环遍历products数组中的对象并对每个对象的price属性加一。
1.添加监听器
用$watch方法来监控作用域中的变化。这一过程对于自定义指令来说要更复杂一些,因为是从一个属性值中获取待计算的表达式,正如你将看到的,这需要一个额外的预备步骤。例子如下:(下面代码还不能工作)
<!DOCTYPE html>
<html ng-app="exampleApp">
<head>
<meta charset="UTF-8">
<title>清单15.10</title>
<link rel="stylesheet" type="text/css" href="bootstrap.css">
<link rel="stylesheet" type="text/css" href="bootstrap-theme.css">
<script src='angular.js'></script>
<script>
angular.module("exampleApp", [] )
.directive("unorderedList", function() {
return function(scope, element, attrs) {
var data = scope[attrs["unorderedList"]];
var propertyExpression = attrs["listProperty"];
if(angular.isArray(data)) {
var listElem = angular.element("<ul>");
element.append(listElem);
for (var i = 0; i < data.length; i++) {
var itemElement = angular.element('<li>');
listElem.append(itemElement);
var watcherFn = function(watchScope) {
return watchScope.$eval(propertyExpression, data[i]);
}
scope.$watch(watcherFn, function(newValue, oldValue) {
itemElement.text(newValue);
});
}
}
}
})
.controller("defaultCtrl", function($scope) {
$scope.products = [
{ name: "Apples", category: "Fruit", price: 1.20, expiry: 10 },
{ name: "Bananas", category: "Fruit", price: 2.42, expiry: 7 },
{ name: "Pears", category: "Fruit", price: 2.02, expiry: 6 }
];
$scope.incrementPrices = function() {
for (var i = 0; i < $scope.products.length; i++) {
$scope.products[i].price++;
console.log($scope.products[i].price);
}
}
})
</script>
</head>
<body ng-controller="defaultCtrl">
<div class="panel panel-default">
<div>
<h3>Products</h3>
</div>
<div>
<button class="btn btn-primary" ng-click="incrementPrices()">Change Prices</button>
</div>
<div>
<div unordered-list="products" list-property="price | currency"></div>
</div>
</div>
</body>
</html>
在本例中,使用了两个函数。第一个函数(监听器函数)基于作用域中的数据计算出一个值,该函数在每次作用域发生变化时都会被调用。如果该函数的返回值发生了变化,处理函数就会被调用,这个过程就像字符串表达式方式一样。
提供一个函数来进行监听,让我能够从容面对表达式中可能包含带有过滤器的数据值的情况。这里定义了这样一个监听器函数:
var watcherFn = function(watchScope) {
return watchScope.$eval(propertyExpression, data[i]);
}
每次监听器函数被重新计算时,该函数被作为参数传递个作用域,另外使用了$eval函数计算在使用的表达式,并使用一个数据对象作为函数值的来源。我们可以将这个监听器函数传给$watch方法并指定毁掉函数,该回调函数使用jqLite的text函数更新li元素的文本内容,以反映数据值的变化。
scope.$watch(watcherFn, function(newValue, oldValue) {
itemElement.text(newValue);
});
效果是指令能够监控被li元素显示的属性值,并在其值被改变时更新元素的内容。
tip:注意这里并没有在$watch处理函数之外设置li元素的内容。AngularJS在指令第一个词被使用时会调用处理器;newValue参数会给出表达式的初始计算值,oldValue参数则为underfined
评论