介绍AngularJS AngularJS是谷歌开发Web应用程序的框架。Angular提供了许多基本的服务,这些服务能够很好地协同工作,并且被设计成可扩展的。这些服务包括数据绑定、DOM操作、路由/视图管理、模块加载等等。 AngularJS不仅仅是另一个图书馆。它提供了一个完整的集成框架,因此减少了必须处理的库的数量。它来自谷歌,同样是开发Chrome并帮助创建下一代网络应用基础的人(更多信息请访问www.polymer-project.org/的聚合物项目)。我相信在5到10年内,我们将不再使用AngularJS来开发web应用程序,但我们将使用类似的东西。 对我来说,AngularJS最令人兴奋的特性是编写自定义指令的能力。自定义指令允许您用新的标记和属性扩展HTML。指令可以在项目内部或跨项目重用,大致相当于。net等平台中的自定义控件。 本文附带的示例包括近50个基于Bootstrap、谷歌JavaScript api和Wijmo创建的自定义指令。该示例有完整的注释并包含文档,因此在开始编写指令时,它可以作为一个很好的参考。您可以在这里看到示例:http://demo.componentone.com/wijmo/Angular/AngularExplorer/AngularExplorer 创建适合您需要的指令相当容易。这些指令可以在多个项目中进行测试、维护和重用。正确实现的指令可以增强和重新部署,只需对使用它们的应用程序进行少量或不进行更改。 本文档主要关注AngularJS指令,但在我们进入这个主题之前,我们将快速浏览一些AngularJS基础知识,以提供上下文。 要使用AngularJS,必须在HTML页面中包含它作为引用,并在页面的HTML或body标签中添加ng-app属性。这里有一个很短的例子来展示它是如何工作的: 隐藏,复制Code
<html> <head> <scriptsrc="http://code.angularjs.org/angular-1.0.1.js"></script> </head> <bodyng-appng-init="msg = 'hello world'"> <inputng-model="msg"/> <p>{{msg}}</p> </body> </html>
当AngularJS加载时,它会扫描文档中的ng-app属性。这个标记通常设置为应用程序的主模块的名称。一旦找到ng-app属性,Angular就会处理文档,加载主模块及其依赖项,扫描文档寻找自定义指令,等等。 在本例中,ng-init属性将一个msg变量初始化为“hello world”,ng-model属性将该变量的内容绑定到一个输入元素。用大括号括起来的文本是一个绑定表达式。AngularJS对表达式求值,并在表达式的值发生变化时更新文档。您可以在这里看到它的作用:jsfiddl .net/ wijmo / hvsqq/ AngularJS模块 模块对象作为AngularJS应用程序的根。它们包含配置、控制器、工厂、过滤器、指令和其他一些对象。 如果你熟悉。net,又不熟悉Angular,下面的表格给出了一个粗略的类比,可以帮助你解释每种类型的AngularJS对象所扮演的角色: AngularJS。NETCommentmoduleAssemblyApplication建筑blockcontrollerViewModelContains应用程序逻辑并公开viewsscopeDataContextProvides数据,可以绑定到视图elementsfilterValueConverterModifies数据之前到达viewdirectiveComponentRe-usable UI elementfactory, serviceUtility classesProvide服务其他模块元素 例如,这段代码创建了一个带有控制器、过滤器和指令的模块: 隐藏,复制Code
var myApp = angular.module("myApp", []); myApp.controller("myCtrl", function($scope) { $scope.msg = "hello world"; }); myApp.filter("myUpperFilter", function() { return function(input) { return input.toUpperCase(); } }); myApp.directive("myDctv", function() { return function(scope, element, attrs) { element.bind("mouseenter", function() { element.css("background", "yellow"); }); element.bind("mouseleave", function() { element.css("background", "none"); }); } });
模块方法以模块名称和依赖项列表作为参数。在本例中,我们创建的模块不依赖于任何其他模块,因此列表为空。注意,数组必须被指定,即使它是空的。省略它将导致AngularJS检索前面指定的已命名模块。我们将在下一节中对此进行更详细的讨论。 控制器构造函数获取一个$scope对象,该对象负责保存控制器公开的所有属性和方法。这个范围将由Angular管理,并传递给视图和指令。在本例中,控制器向范围添加了一个msg属性。应用程序模块可能有多个控制器,每个控制器负责一个或多个视图。控制器不必成为模块的成员,但是让它们成为成员是一个很好的实践。 过滤器构造函数返回一个用于修改输入以进行显示的函数。Angular提供了几个过滤器,但你可以添加自己的过滤器,并以完全相同的方式使用它们。在本例中,我们定义了一个将字符串转换为大写的过滤器。过滤器不仅可以用来格式化值,还可以用来修改数组。AngularJS提供的格式过滤器包括数字、日期、货币、大写和小写。数组过滤器包括过滤器、orderBy和limitTo。过滤器可以使用参数,并且语法总是相同的:someValue | filterName:filterParameter1:filterParameter2… 指令构造函数返回一个函数,该函数接受一个元素,并根据在作用域中定义的参数对其进行修改。在本例中,我们将事件处理程序绑定到mouseenter和mouseleave事件,以便当鼠标在元素内容上时突出显示它。这是我们的第一个指令,仅仅触及了指令的表面。AngularJS指令可以用作属性或元素(甚至是注释),它们可以嵌套并相互通信。我们将在后面的部分中详细介绍。 下面是使用此模块的页面: 隐藏,复制Code
<bodyng-app="myApp"ng-controller="myCtrl"> <inputng-model="msg"/> <pmy-dctv> {{ msg | myUpperFilter }} </p> </body>
您可以在这里看到它的作用:jsfiddl .net/ wijmo / jkbbv / 注意,app模块、控制器和过滤器的名称被用作属性值。它们表示JavaScript对象,因此这些名称是区分大小写的。 另一方面,指令的名称被用作属性名称。它表示一个HTML元素,因此不区分大小写。然而,AngularJS将驼线格式的指令名称转换为用连字符分隔的字符串。这样“myDctv”指令就变成了“my-dctv”(就像内置的指令ngApp、ngController和ngModel变成了“ng-app”、“ng-controller”和“ng-model”一样。 项目组织 AngularJS被设计用来处理大型项目。您可以将项目分解为多个模块,将模块分解为多个文件,并以任何对您有意义的方式组织这些文件。我所见过的大多数项目都倾向于遵循Brian Ford在他的博客中用AngularJS构建huuuuuuge Apps中建议的惯例。一般的想法是将模块分解成文件,并按类型对它们进行分组。因此,控制器被放在controllers文件夹中(命名为XXXCtrl),指令被放在directive文件夹中(命名为XXXDctv),等等。 一个典型的项目文件夹看起来是这样的: 隐藏,复制Code
Root default.html styles app.css partials home.html product.html store.html scripts app.js controllers productCtrl.js storeCtrl.js directives gridDctv.js chartDctv.js filters formatFilter.js services dataSvc.js vendor angular.js angular.min.js
例如,假设您想使用在app.js文件中定义的单个模块。你可以这样定义它: 隐藏,复制Code
// app.js angular.module("appModule", []);
要向模块添加元素,您需要按名称请求模块并向其添加元素,如前面所示。例如,formatFilter.js文件会包含如下内容: 隐藏,复制Code
// formatFilter.js // retrieve module by name var app = angular.module("appModule"); // add a filter to the module app.filter("formatFilter", function() { return function(input, format) { return Globalize.format(input, format); } }})
如果应用程序包含多个模块,请记住在创建每个模块时指定依赖关系。例如,一个包含三个模块app、控件和data的应用程序可以如下指定它们: 隐藏,复制Code
// app.js (the main application module, depends on "controls" and "data" modules) angular.module("app", [ "controls", "data"]) // controls.js (the controls module, depends on "data" module) angular.module("controls", [ "data" ]) // data.js (the data module, no dependencies) angular.module("data", [])
应用程序的主页会在ng-app指令中指定主模块的名称,AngularJS会自动引入所有需要的依赖项: 隐藏,复制Code
<htmlng-app="app"> ... </html>
主页及其所有视图将能够使用在这三个模块中定义的元素。 要了解以上述方式组织的相当大的应用程序示例,请参阅本文附带的AngularExplorer示例。 既然我们已经介绍了AngularJS的基础知识,现在就来处理我们的主要主题:指令。在接下来的几章中,我们将介绍这些基本概念,并创建一些指令来演示它们的可能性,这是相当令人惊奇的。 如果你想在继续之前(或者在任何时候)学习更多关于角的知识,我推荐Dan Wahling的优秀视频“60分钟左右的角的基本原理”。在“关于那些指令”页面上,也有一些由AngularJS团队成员制作的有趣视频。 AngularJS指令:为什么? 我之前说过,对我来说,指令是AngularJS最令人兴奋的特性。这是因为它们是角兔唯一的特征。AngularJS的其他特性也可以在其他框架中使用。但是,创建可重用的组件库并将其添加到纯HTML应用程序中的能力是非常强大的,据我所知,AngularJS是目前唯一为web应用程序提供这种能力的框架。 有几种JavaScript产品为web开发人员提供控件。例如,Boostrap是一个流行的“前端框架”,它提供了样式和一些JavaScript组件。问题是,为了使用这些组件,HTML作者必须切换到JavaScript模式并编写jQuery代码来激活这些选项卡。jQuery代码非常简单,但必须与HTML同步,这是一个冗长且容易出错的过程,而且伸缩性不好。 AngularJS的主页显示了一个简单的指令,它包装了引导标签组件,使它在纯HTML中使用起来非常容易。该指令使制表符像有序列表一样易于使用。另外,该指令可以被许多HTML开发人员在许多项目中重用。HTML和t一样简单他: 隐藏,复制Code
<bodyng-app="components"> <h3>BootStrap Tab Component</h3> <tabs> <panetitle="First Tab"> <div>This is the content of the first tab.</div> </pane> <panetitle="Second Tab"> <div>This is the content of the second tab.</div> </pane> </tabs> </body>
您可以在这里看到它的作用:jsfiddle.net/Wijmo/ywUYQ/ 正如您所看到的,这个页面看起来像普通的HTML,除了它已经被扩展为tabs>和& lt; pane>作为指令实现的标签。HTML开发人员不需要编写任何JavaScript。当然,必须有人编写指令,但这些都是通用的。它们可以编写一次,然后多次重用(就像BootStrap、jQueryUI、Wijmo和所有其他伟大的库)。 由于指令非常有用,而且编写起来也不那么困难,许多人已经开始为流行的库创建指令了。例如,AngularJS团队已经为Boostrap创建了一组名为UI Bootstrap的指令;ComponentOne船舶AngularJS指令与它的Wijmo库;jQueryUI小部件有几个公共指令存储库。 但是等一下!如果有这么多现成的指令来源,为什么要学习如何自己创建它们呢?好问题。也许你不喜欢。所以在写你自己的书之前先看看。但这里有几个学习的好理由: 你可能有特殊的需要。例如,假设您为一家金融公司工作,该公司在许多应用程序中使用某种类型的表单。该表单可以实现为一个数据网格,具有自定义功能,以某种方式下载数据,以某种方式编辑和验证数据,并以某种方式将更改上传回服务器。你公司以外的人不太可能拥有对你有用的东西。但是你可以编写一个自定义指令,让你的团队中的所有HTML开发人员都可以使用它,这样他们就可以编写:Hide Code<复印件;身体ng-app =“abcFinance”比; & lt; h3>海外投资Summary< / h3> & lt; abc-investment-form 客户= " currentCustomer " 国家=“currentCountry”比; & lt; / abc-investment-form data> & lt; / body> “abcInvestmentForm”指令可以在许多应用程序中使用,以提供一致性。指令将集中维护,并可进行更新,以反映新的业务实践或要求,而对应用程序的影响很小。 也许你想要的指令还不存在。也许您碰巧喜欢一个还没有人为其编写指令的库,并且您不想等待。或者您只是不喜欢您所找到的指令,您想调整它们。 好吧,我想,如果您正在阅读这篇文章,那么您已经对指令的概念很感兴趣,并且渴望开始学习。我们继续。 AngularJS指令:如何? 我们在本文开头展示的指令非常简单。它只指定了一个“链接”功能,没有指定其他功能。一个典型的指令包含更多的元素: 隐藏,复制Code
// create directive module (or retrieve existing module) var m = angular.module("myApp"); // create the "my-dir" directive myApp.directive("myDir", function() { return { restrict: "E", // directive is an Element (not Attribute) scope: { // set up directive's isolated scope name: "@", // name var passed by value (string, one-way) amount: "=", // amount var passed by reference (two-way) save: "&" // save action }, template: // replacement HTML (can use our scope vars here) "<div>" + " {{name}}: <input ng-model='amount' />" + " <button ng-click='save()'>Save</button>" + "</div>", replace: true, // replace original markup with template transclude: false, // do not copy original HTML content controller: [ "$scope", function ($scope) { … }], link: function (scope, element, attrs, controller) {…} } });
注意指令名是如何遵循一种模式的:“my”前缀类似于名称空间,因此如果应用程序使用来自多个模块的指令,那么很容易确定它们的定义位置。这不是必需的,但它是一个很有意义的推荐实践。 指令构造函数返回具有几个属性的对象。这些都记录在AngularJS网站上,但他们提供的解释总是像他们应该的那样清楚。下面是我试图解释这些属性的作用: 限制:决定指令是否将在HTML中使用。对于属性、元素、类或注释,有效的选项是“A”、“E”、“C”和“M”。默认值是“A”,表示属性。但我们更感兴趣的是元素属性,因为这是创建UI元素的方式,如前面所示的“tab”指令。作用域:创建一个属于指令的隔离作用域,将其与调用者的作用域隔离。作用域变量作为属性在指令标记中传入。这种隔离在创建可重用组件时非常重要,不应该依赖于父范围。作用域对象定义作用域变量的名称和类型。上面的例子定义了三个范围变量: 名称:“@”(按值,单向): @符号“@”表示变量是按值传递的。指令接收一个字符串,其中包含从父作用域传入的值。指令可以使用它,但它不能改变父范围内的值(它是隔离的)。这是最常见的变量类型。金额:"="(引用,双向) 等号"="表示通过引用传递这个变量。该指令接收对主范围内值的引用。值可以是任何类型,包括复杂对象和数组。该指令可能会更改父范围内的值。这种类型的变量时使用该指令需要改变父母中的值范围(例如一个编辑器控件),当的值是一个复杂的类型不能被序列化为一个字符串,或者当值是一个大阵,将昂贵的序列化为一个字符串。保存:“,”(表达) 我的persand "&"表示该变量持有一个在父作用域上下文中执行的表达式。它允许指令执行操作,而不仅仅是简单地更改值。 模板:替换原始标记中的元素的字符串。替换过程将所有属性从旧元素迁移到新元素。注意模板是如何使用在隔离范围中定义的变量的。这允许您编写不需要任何额外代码的宏样式指令。然而,在大多数情况下,模板只是一个空模板。它将使用下面讨论的链接函数中的代码填充。replace:确定指令模板是否应该替换原始标记中的元素,还是将其追加到原始标记中。默认值为false,这将导致保留原始标记。transclude:确定自定义指令是否应该复制原始标记中的内容。例如,前面显示的“tab”指令被transclude设置为true,因为tab元素包含其他HTML元素。另一方面,“dateInput”指令没有HTML内容,所以您可以将transclude设置为false(或者干脆忽略它)。这个函数包含了大部分指令逻辑。它负责执行DOM操作、注册事件监听器等。链接函数接受以下参数: 范围:引用指令的孤立范围。范围变量最初是未定义的,链接函数注册监视以在它们的值发生变化时接收通知。元素:引用包含指令的DOM元素。链接函数通常使用jQuery(或者Angular的jqLite,如果没有加载jQuery)操作这个元素。控制器:用于有嵌套指令的场景。此参数为子指令提供对父指令的引用,从而允许指令进行通信。前面讨论的选项卡指令就是一个很好的例子:jsfiddl .net/ wijmo / ywuyq/ 注意,当调用link函数时,通过值("@")传递的范围变量还没有初始化。它们将在指令生命周期的后期被初始化,如果你想接收通知,你必须使用范围。$watch函数,将在下一节中讨论。 如果您还不熟悉指令,真正理解所有这些的最好方法是使用一些代码并尝试不同的东西。这个小提琴允许您这样做:jsfidder .net/ wijmo / lyj2t/ fiddle定义了一个具有三个成员(customerName、credit和save)的控制器。它还定义了一个类似于上面列出的指令,具有具有三个成员(名称、数量和保存)的独立作用域。该HTML显示了如何使用纯HTML和指令的控制器。尝试更改标记、隔离变量的类型、模板等等。这将使您对指令的工作方式有一个很好的了解。 指令和父作用域之间的通信 好的,所以指令应该有自己独立的作用域,这样它们就可以在不同的项目中重用,并绑定到不同的父作用域。但是这些作用域到底是如何通信的呢? 例如,假设你有一个独立的范围声明的指令在上面的例子: 隐藏,复制Code
scope: { // set up directive's isolated scope name: "@", // name var passed by value (string, one-way) amount: "=", // amount var passed by reference (two-way) save: "&" // save command },
并假设指令是在这种情况下使用的: 隐藏,复制Code
<my-dirname="{{customerName}}"amount="customerCredit"save="saveCustomer()"/>
注意“name”属性是如何用花括号括起来的,而“amount”不是。这是因为“name”是按值传递的。如果没有方括号,值将被设置为字符串“customerName”。括号使AngularJS对前面的表达式求值,并将属性值设置为结果。相比之下,“amount”是一个参考,所以你不需要括号。 指令可以检索范围变量的值,只需从范围对象中读取它们: 隐藏,复制Code
var name = scope.name; var amount = scope.amount;
这确实会返回变量的当前值,但是如果值在父范围内发生了变化,指令将不知道它。要得到这些变化的通知,它必须向这些表达式添加监视器。这可以在范围内完成。$watch方法,其定义为: 隐藏,复制Code
scope.$watch(watchExpression, listenerFunction, objectEquality);
watch表达式是您想要查看的东西(在我们的示例中,是“name”和“amount”)。listenerFunction是在表达式更改值时调用的函数。这个函数负责更新指令以反映新的值。 最后一个参数objectEquality决定了AngularJS应该如何比较变量的新旧值。如果将objectEquality设置为true,那么AngularJS将对新旧值进行深度比较,而不是简单的引用比较。当范围变量是引用("=")而不是值("@")时,这一点非常重要。例如,如果变量是数组或复杂对象,将objectEquality设置为true将导致listene即使变量仍然引用相同的对象数组,但是数组或对象的内容已经改变,也要调用。 回到我们的例子,你可以观察变化的范围变量使用以下代码: 隐藏,复制Code
scope.$watch("name", function(newValue, oldValue, srcScope) { // handle the new value of "name" }); scope.$watch("amount", function(newValue, oldValue, srcScope) { // handle the new value of "amount" });
注意,listenerFunction会传递新值和旧值以及scope对象本身。您很少需要这些参数,因为在范围上已经设置了新值,但是在某些情况下,您可能需要检查到底更改了什么。在一些罕见的情况下,新值和旧值实际上可能是相同的。当指令初始化时可能会发生这种情况。 另一个方向呢?在我们的示例中,“amount”变量是对一个值的引用,父作用域可能会像我们一样监视它的变化。 在大多数情况下,你什么都不用做。AngularJS自动检测由于用户交互而发生的变化,并为你处理所有的观察者。但情况并非总是如此。由于浏览器DOM事件、setTimeout、XHR或第三方库而发生的更改不会被Angular检测到。在这些情况下,应该调用作用域。方法,它将把更改广播给所有已注册的侦听器。 例如,假设我们的指令有一个名为updateAmount的方法,它执行一些计算并更改“amount”属性的值。这里是你将如何实现: 隐藏,复制Code
function updateAmount() { // update the amount value scope.amount = scope.amount * 1.12; // inform listeners of the change if (!scope.$$phase) scope.$apply("amount"); }
的范围。$$phase变量是由AngularJS在更新范围变量时设置的。我们测试这个变量,以避免在更新周期内调用$apply。 总结、范围。$watch处理入站更改通知和范围。$apply处理出站更改通知(但您很少需要调用它)。 通常,真正理解某件事的最好方式是观察它的实际行动。在jsfiddle.net/Wijmo/aX7PY/的小提琴定义了一个控制器和一个指令。两者都有更改数组中的数据的方法,并且都侦听彼此应用的更改。尝试注释掉对范围的调用。美元的手表和范围。$申请看看他们的效果。 共享代码/依赖注入 当您开始编写指令时,您可能会创建对许多指令都有用的实用程序方法。当然,您不希望复制这些代码,因此对这些实用程序进行分组并将它们公开给所有需要它们的指令是有意义的。 您可以通过向包含指令的模块中添加工厂,然后在指令构造函数中指定工厂名称来实现这一点。例如: 隐藏,收缩,复制Code
// the module var app = angular.module("app", []); // utilities shared by all directives app.factory("myUtil", function () { return { // watch for changes in scope variables // call update function when all have been initialized watchScope: function (scope, props, updateFn, updateOnTimer) { var cnt = props.length; angular.forEach(props, function (prop) { scope.$watch(prop, function (value) { if (--cnt <= 0) { if (updateOnTimer) { if (scope.updateTimeout) clearTimeout(scope.updateTimeout); scope.updateTimeout = setTimeout(updateFn, 50); } else { updateFn(); } } }) }) }, // change the value of a scope variable and notify listeners apply: function (scope, prop, value) { if (scope[prop] != value) { scope[prop] = value; if (!scope.$$phase) scope.$apply(prop); } } ) });
上面列出的“myUtil”工厂包含两个实用函数: watchScope为几个范围变量添加监视器,并在其中任何一个变量发生变化(指令初始化期间除外)时调用更新函数。它可以选择使用超时来避免过于频繁地调用更新函数。apply更改范围变量的值,并将更改通知侦听器(除非新值与前一个值相同)。 要从自定义指令中使用这些实用函数,你需要写: 隐藏,复制Code
app.directive("myDir", ["$rootScope", "myUtil", function ($rootScope, myUtil) { return { restrict: "E", scope: { v1: "@", v2: "@", v3: "@", v4: "@", v5: "@", v6: "@" }, template: "<div/>", link: function (scope, element, attrs) { var ctr = 0, arr = ["v1", "v2", "v3", "v4", "v5", "v6"]; myUtil.watchScope(scope, arr, updateFn); function updateFn() { console.log("# updating my-dir " + ++ctr); // modify DOM here } } } }]);
如您所见,我们只是将“myUtil”工厂添加到指令构造函数中,使其所有方法对指令可用。 您可以在jsfidder .net/ wijmo / gjm9m/中看到这段代码的作用 尽管表面上很简单,但有很多有趣的事情让它发挥作用。AngularJS检查了指令,检测到了“myUtil”参数,在模块定义中按名称找到了“myUtil”工厂,并在正确的位置注入了引用。依赖注入是一个深入的主题,在AngularJS文档中有描述。 依赖注入机制依赖于名称这一事实产生了一个与缩小相关的问题。当您缩小代码以投入生产时,变量名会发生变化,这可能会破坏依赖注入。为了解决这个问题,AngularJS允许您使用数组语法声明模块元素,其中包括参数名作为字符串。如果您查看上面的指令定义代码,就会注意到该声明包含一个数组,其参数名(在本例中仅为“myUtil”)后面跟着实际的构造函数。这允许AngularJS按名称查找“myUtil”工厂,即使缩小过程改变了构造函数参数的名称。 关于缩小的重要提示:如果你计划缩小你的指令,你必须在所有带参数的指令上使用数组声明技术,在控制器声明中也要包含参数。这一事实没有得到很好的证明,并且会阻止带有控制器函数的指令在缩小之后工作。例如,Angular主页上列出的引导标签指令是不可缩小的,但这个是:jsfiddl .net/ wijmo / ywuyq/。 除了工厂外,AngularJS还包括三个其他类似的概念:提供者、服务和价值。两者之间的区别很微妙。从我开始使用Angular开始,我就一直在使用工厂,到目前为止,我还不需要任何其他的风格。 例子 既然我们已经复习了所有的基础知识,现在就来复习几个示例,以展示这些内容在实践中是如何工作的。接下来的部分描述了一些有用的指令,它们说明了要点,应该可以帮助您开始编写自己的指令。 引导手风琴指令 我们的第一个例子是一对创建引导accordions的指令: 引导手风琴样本 Bootstrap网站有一个例子,展示了如何使用纯HTML创建一个手风琴: 隐藏,收缩,复制Code
<divclass="accordion"id="accordion2"> <divclass="accordion-group"> <divclass="accordion-heading"> <aclass="accordion-toggle"data-toggle="collapse"data-parent="#accordion2"href="#collapseOne"> Collapsible Group Item #1 </a> </div> <divid="collapseOne"class="accordion-body collapse in"> <divclass="accordion-inner"> Anim pariatur cliche... </div> </div> </div> <divclass="accordion-group"> <divclass="accordion-heading"> <aclass="accordion-toggle"data-toggle="collapse"data-parent="#accordion2"href="#collapseTwo"> Collapsible Group Item #2 </a> </div> <divid="collapseTwo"class="accordion-body collapse"> <divclass="accordion-inner"> Anim pariatur cliche... </div> </div> </div> </div>
这可以工作,但需要大量的标记。标记包含基于href和元素id的引用,这使得维护变得非常重要。 使用自定义指令你可以得到相同的结果使用这个HTML: 隐藏,复制Code
<btst-accordion> <btst-panetitle="<b>First</b> Pane"> <div>Anim pariatur cliche … </btst-pane> <btst-panetitle="<b>Second</b> Pane"> <div>Anim pariatur cliche … </btst-pane> <btst-panetitle="<b>Third</b> Pane"> <div>Anim pariatur cliche … </btst-pane> </btst-accordion>
这个版本更小,更容易阅读和维护。 让我们看看这是怎么做的。首先,我们定义了一个模块和“btstAccordion”指令: 隐藏,收缩,复制Code
var btst = angular.module("btst", []); btst.directive("btstAccordion", function () { return { restrict: "E", // the Accordion is an element transclude: true, // it has HTML content replace: true, // replace the original markup with our template scope: {}, // no scope variables required template: // template assigns class and transclusion element "<div class='accordion' ng-transclude></div>", link: function (scope, element, attrs) { // make sure the accordion has an id var id = element.attr("id"); if (!id) { id = "btst-acc" + scope.$id; element.attr("id", id); } // set data-parent and href attributes on accordion-toggle elements var arr = element.find(".accordion-toggle"); for (var i = 0; i < arr.length; i++) { $(arr[i]).attr("data-parent", "#" + id); $(arr[i]).attr("href", "#" + id + "collapse" + i); } // set collapse attribute on accordion-body elements // and expand the first pane to start arr = element.find(".accordion-body"); $(arr[0]).addClass("in"); // expand first pane for (var i = 0; i < arr.length; i++) { $(arr[i]).attr("id", id + "collapse" + i); } }, controller: function () {} }; });
指令设置为true,因为它有HTML内容。模板使用ng-transclude指令指示哪个模板元素将接收经过transclude的内容。在本例中,模板只有一个元素,因此没有其他选项,但情况并非总是如此。 代码中有趣的部分是link函数。首先要确保accordion元素有一个id,如果没有,代码会根据指令作用域的id创建一个唯一的id。一旦元素有了ID,函数就使用jQuery选择类为“accordion-toggle”的子元素,并设置它们的“data-parent”和“href”属性。最后,代码查找“手风琴体”元素并设置它们的“collapse”属性。 指令还包括一个包含空函数的控制器成员。这是必需的,因为accordion将具有子元素,这些子元素将检查父元素的类型是否正确,并指定一个控制器。 下一步是定义accordion窗格指令。这是非常简单的,大部分的动作发生在模板中,几乎没有代码: 隐藏,收缩,复制Code
btst.directive('btstPane', function () { return { require: "^btstAccordion", restrict: "E", transclude: true, replace: true, scope: { title: "@" }, template: "<div class='accordion-group'>" + " <div class='accordion-heading'>" + " <a class='accordion-toggle' data-toggle='collapse'>{{title}}</a>" + " </div>" + "<div class='accordion-body collapse'>" + " <div class='accordion-inner' ng-transclude></div>" + " </div>" + "</div>", link: function (scope, element, attrs) { scope.$watch("title", function () { // NOTE: this requires jQuery (jQLite won't do html) var hdr = element.find(".accordion-toggle"); hdr.html(scope.title); }); } }; });
require成员指定“btstPane”指令必须在“btstAccordion”中使用。transclude成员表示窗格将具有HTML内容。范围有一个“title”属性,它将被放置在窗格标题中。 在本例中,模板相当复杂。它是直接从引导程序示例页面复制的。请注意,我们使用ng-transclude指令来标记将接收经过transclude内容的元素。 我们可以停在这里。模板中包含的"{{title}}"属性足以在适当的位置显示标题。但是,这种方法只允许在窗格标题中使用纯文本。我们使用link函数将纯文本替换为HTML,这样就可以在折叠式标题中包含丰富的内容。 就是这样。我们已经完成了第一对有用的指令。它们很小,但说明了一些重要的要点和技术:如何定义嵌套指令,如何生成惟一的元素id,如何使用jQuery操作DOM,以及如何使用$watch函数侦听作用域变量的变化。 谷歌地图指示 下一个例子是一个创建谷歌映射的指令: 谷歌映射指令示例 在我们开始工作之前的指令,记得添加一个参考的谷歌api到HTML页: 隐藏,复制Code
<!-- required to use Google maps --> <scripttype="text/javascript"src="https://maps.googleapis.com/maps/api/js?sensor=true"> </script>
接下来,让我们定义指令: 隐藏,复制Code
var app = angular.module("app", []); app.directive("appMap", function () { return { restrict: "E", replace: true, template: "<div></div>", scope: { center: "=", // Center point on the map markers: "=", // Array of map markers "@", // Map width in pixels. height: "@", // Map height in pixels. zoom: "@", // Zoom level (from 1 to 25). mapTypeId: "@" // roadmap, satellite, hybrid, or terrain },
center属性被定义为引用("="),因此它将支持双向绑定。应用程序可以改变中心并通知地图(当用户通过点击按钮选择位置时),地图也可以改变中心并通知应用程序(当用户通过滚动地图选择位置时)。 marker属性也被定义为by reference,因为它是一个数组,将其序列化为字符串可能会很耗时(但也可以工作)。 本例中的link函数包含相当多的代码。它必须: 初始化映射,在范围变量变化时更新映射,并侦听映射事件并更新范围。 这是如何做到的: 隐藏,复制Code
link: function (scope, element, attrs) { var toResize, toCenter; var map; var currentMarkers; // listen to changes in scope variables and update the control var arr = ["width", "height", "markers", "mapTypeId"]; for (var i = 0, cnt = arr.length; i < arr.length; i++) { scope.$watch(arr[i], function () { if (--cnt <= 0) updateControl(); }); } // update zoom and center without re-creating the map scope.$watch("zoom", function () { if (map && scope.zoom) map.setZoom(scope.zoom * 1); }); scope.$watch("center", function () { if (map && scope.center) map.setCenter(getLocation(scope.center)); });
监视作用域变量的函数与前面讨论共享代码时描述的函数类似。当变量发生任何更改时,它调用updateControl函数。updateControl函数实际上使用当前选择的选项创建映射。 “缩放”和“中心”的范围变量被区别对待,因为我们不想在每次用户选择一个新位置或缩放或缩小时重新创建地图。这两个函数检查映射是否已经创建并简单地更新它。 下面是updateControl函数的实现: 隐藏,收缩,复制Code
// update the control function updateControl() { // get map options var options = { center: new google.maps.LatLng(40, -73), zoom: 6, mapTypeId: "roadmap" }; if (scope.center) options.center = getLocation(scope.center); if (scope.zoom) options.zoom = scope.zoom * 1; if (scope.mapTypeId) options.mapTypeId = scope.mapTypeId; // create the map and update the markers map = new google.maps.Map(element[0], options); updateMarkers(); // listen to changes in the center property and update the scope google.maps.event.addListener(map, 'center_changed', function () { if (toCenter) clearTimeout(toCenter); toCenter = setTimeout(function () { if (scope.center) { if (map.center.lat() != scope.center.lat || map.center.lng() != scope.center.lon) { scope.center = { lat: map.center.lat(), lon: map.center.lng() }; if (!scope.$$phase) scope.$apply("center"); } } }, 500); }
updateControl函数首先准备一个反映范围设置的options对象,然后使用options对象创建和初始化映射。这是创建封装JavaScript小部件的指令时的常见模式。 创建映射之后,该函数更新标记并添加一个事件处理程序,以便在映射中心发生更改时通知它。事件处理程序检查当前地图中心是否与作用域的center属性不同。如果是,则处理程序更新范围并调用$apply函数,这样AngularJS就会通知任何侦听器属性已经更改。这就是双向绑定如何在角绳中工作。 updateMarkers函数非常简单,不包含任何与AngularJS直接相关的内容,所以我们不在这里列出它。 除了map指令,这个例子还包括: 两个过滤器,转换坐标表示为常规数字到地理位置,如33°38'24"N, 85°49'2"W。将地址转换为地理位置的地理编码器(同样基于谷歌api)。使用HTML5地理定位服务获取用户当前位置的方法。 谷歌的映射api非常丰富。这个指令仅仅触及了您可以使用它做的事情的皮毛,但是如果您对开发位置感知应用程序感兴趣的话,希望它足以让您入门。 您可以在这里找到谷歌的映射api文档:https://developers.google.com/maps/documentation/ 你可以在这里找到谷歌的许可条款:https://developers.google.com/maps/licensing Wijmo图指令 下一个例子是一个显示实验数据和线性回归的图表。这个示例演示了前面描述的场景,在这个场景中,您有一种特殊的需求,这种需求不太可能被商业产品附带的标准指令所满足: Wijmo图表指令示例 这个图表指令基于Wijmo线图小部件,它是这样使用的: 隐藏,复制Code
<app-chartdata="data"x="x"y="y"reg-parms="reg"color="blue"> </app-chart>
参数如下: 数据:具有plotx、y属性的对象列表:将显示在x和y轴对称上的属性名称:线性回归结果,具有代表回归参数和决定系数(即R2)的属性的对象。颜色:图表上符号的颜色。 在指令的最初版本中,回归是在图表本身中计算的,“reg”参数是不需要的。但我认为这不是正确的设计,因为回归参数在图表之外很重要,因此应该在控制器的范围内计算。 言归正传,这里是指令实现: 隐藏,复制Code
app.directive("appChart", function (appUtil) { return { restrict: "E", replace: true, scope: { data: "=", // array that contains the data for the chart. x: "@", // property that contains the X values. y: "@", // property that contains the Y values. regParms: "=", // regression parameters (a and b coefficients) color: "@" // color for the data series. }, template: "<div></div>", link: function (scope, element, attrs) { // watch for changes in the scope variables appUtil.watchScope(scope, ["x", "y", "color"], updateChartControl, true, true); // update chart data when data changes scope.$watch("data", updateChartData);
第一个代码块像往常一样定义了指令类型和范围。link函数使用前面介绍的watchScope方法来监视几个作用域变量,并在任何作用域变量发生变化时调用updateChartControl方法。 请注意,我们使用了对作用域的单独调用。$watch data因为我们希望图表数据比其他属性变化得更频繁,所以我们将提供一个更有效的调用updateChartData来处理这些变化。 下面是updateChartControl方法的实现,它实际创建了图表。 隐藏,收缩,复制Code
// create/update the chart control function updateChartControl(prop, val) { // use element font in the chart var fontFamily = element.css("fontFamily"); var fontSize = element.css("fontSize"); var textStyle = { "font-family": fontFamily, "font-size": fontSize }; // set default values var color = scope.color ? scope.color : "red"; // build options var options = { seriesStyles: [ { stroke: color, "stroke-width": 0 }, { stroke: "black", "stroke-width": 1, "stroke-opacity": .5 } ], seriesHoverStyles: [ { stroke: color, "stroke-width": 0 }, { stroke: "black", "stroke-width": 2, "stroke-opacity": 1 } ], legend: { visible: false }, showChartLabels: false, animation: { enabled: false }, seriesTransition: { enabled: false }, axis: { x: { labels: { style: textStyle }, annoFormatString: "n0" }, y: { labels: { style: textStyle }, annoFormatString: "n0" } }, textStyle: textStyle }; // create the chart element.wijlinechart(options); // go update the chart data updateChartData(); }
该代码类似于我们在前面的谷歌映射指令中使用的代码。它构建一个包含配置信息的options对象,其中一些是基于指令参数的,然后使用这个options对象通过调用元素来创建实际的图表。wijlinechart方法。 创建图表小部件之后,代码调用updateChartData方法来填充图表。updateChartData方法创建两个数据系列。第一个表示通过范围变量传入的数据,第二个表示回归。第一个系列的数据点与控制器传入的数据点一样多,并以符号表示。第二个级数表示线性回归,因此只有两个点。它用实线表示。 Wijmo电网指令 最后一个例子是一个指令,它实现了一个可编辑的数据网格: Wijmo网格指令示例 这个指令是基于Wijmo网格小部件,是这样使用的: 隐藏,复制Code
<wij-griddata="data"allow-editing="true"after-cell-edit="cellEdited(e, args)"> <wij-grid-columnbinding="country"width="100"group="true"> </wij-grid-column> <wij-grid-columnbinding="product"width="140"> </wij-grid-column> <wij-grid-columnbinding="amount"width="100"format="c2"aggregate="sum"> </wij-grid-column> </wij-grid>
“wij-grid”指令指定网格的属性,“wij-grid-column”指令指定各个网格列的属性。上面的标记定义了一个可编辑的网格,有三列“国家”、“产品”和“数量”。值按国家分组,分组行显示每个组的总金额。 这个指令最有趣的部分是父指令“wij-grid”和子指令“wij-grid-column”之间的连接。为了启用这个连接,父指令指定了如下控制器函数: 隐藏,复制Code
app.directive("wijGrid", [ "$rootScope", "wijUtil", function ($rootScope, wijUtil) { return { restrict: "E", replace: true, transclude: true, template: "<table ng-transclude/>", scope: { data: "=", // List of items to bind to. allowEditing: "@", // Whether user can edit the grid. afterCellEdit: "&", // Event that fires after cell edits. allowWrapping: "@", // Whether text should wrap within cells. frozenColumns: "@" // Number of non-scrollable columns }, controller: ["$scope", function ($scope) { $scope.columns = []; this.addColumn = function (column) { $scope.columns.push(column); } }], link: function (scope, element, attrs) { // omitted for brevity, see full source here: // http://jsfiddle.net/Wijmo/jmp47/ } } }]);
controller函数是使用前面提到的数组语法声明的,因此它可以被缩小。在本例中,控制器定义了一个addColumn函数,它将由子“wij-grid-column”指令调用。然后父指令将有权访问标记中指定的列信息。 下面是“wij-grid-column”指令如何使用这个函数: 隐藏,复制Code
app.directive("wijGridColumn", function () { return { require: "^wijGrid", restrict: "E", replace: true, template: "<div></div>", scope: { binding: "@", // Property shown in this column. header: "@", // Column header content. format: "@", // Format used to display numeric values in this column. "@", // Column width in pixels. aggregate: "@", // Aggregate to display in group header rows. group: "@", // Whether items should be grouped by the values in this column. groupHeader: "@" // Text to display in the group header rows. }, link: function (scope, element, attrs, wijGrid) { wijGrid.addColumn(scope); } } });
require成员指定“wij-grid-column”指令需要一个类型为“wij-grid”的父指令。link函数接收到对父指令(控制器)的引用,并使用addColumn方法将自己的作用域传递给父指令。范围包含网格创建列所需的所有信息。 更多的指令 除了本文讨论的示例之外,附加的示例还包含近50个其他指令,您可以使用和修改它们。示例应用程序本身的结构遵循这里建议的原则,因此导航它应该没有问题。 在示例中,指令可以在scripts/directives文件夹下的三个文件中找到: btstDctv:包含13个基于引导库的指令。这些指令包括选项卡、手风琴、弹出窗口、工具提示、菜单、typeahead和numericInput。googleDctv:包含两个基于谷歌的JavaScript api的指令:一个地图和一个图表。wijDctv:包含24个基于Wijmo库的指令。这些指令包括输入、布局、网格和图表。 所有三个指令模块都包括在源和缩小的格式。我们使用了谷歌的闭包缩小器,您可以在这里在线使用它:http://closurecompiler.appspot.com/home。 这里有一个Angular浏览器示例的在线版本:http://demo.componentone.com/wijmo/Angular/AngularExplorer/AngularExplorer。 结论 我希望您在阅读这篇文章时得到乐趣,并且像我一样对AngularJS和自定义指令感到兴奋。 请随意使用样本中的代码,如果您有任何反馈,请与我联系。我特别感兴趣的是关于新指令的想法,以及如何使这些指令更强大和有用。 参考文献 AngularJS谷歌。AngularJS的主页。AngularJS指示文档。关于AngularJS指令的官方文档。AngularJS指令和JavaScript的计算机科学。关于编写AngularJS指令的有趣文章。视频教程:AngularJS基本原理60分钟左右。由Dan Wahling制作的介绍角兔的视频。对这些指令。由AngularJS团队成员制作的指令系列视频。egghead .io。约翰·林德奎斯特关于AngularJS的一系列指导视频。聚合物的项目。角鲨之后会发生什么。Wijmo AngularJS样本。几个使用AngularJS和自定义指令创建的在线演示。 本文转载于:http://www.diyabc.com/frontweb/news17320.html