Angular JS - 使用指令创建自定义属性






4.44/5 (7投票s)
介绍 Angular 的指令以及如何使用它们来创建自己的属性,
引言
Angular是一个相当新的JavaScript库,它为Web界面开发提供了声明式数据绑定功能。这使您能够使用MVVM设计模式开发Web UI。本文的第一版没有花时间解释Angular的核心点或MVVM设计模式。Angularjs.org可以帮助您入门。我还没有决定是否要全面介绍Angular的功能作为引言。我也没有决定是将其作为本文的一部分,还是作为本文的补充单独撰写一篇文章。
为了本文的目的,我将自定义属性的概念与Angular特有的术语“指令”互换使用。
背景
我个人在Web开发和WPF中的MVVM设计方面做了很多工作,因此我特别兴奋地尝试Angular。我对它所能实现的一些功能感到非常满意。我才刚刚开始探索Angular的功能和特性,但它能够以使标记轻量化并使您编写的JavaScript保持简单的方式添加自定义属性,这给我留下了特别深刻的印象。
项目解释
本文无意全面解释如何使用Angular。不幸的是,我觉得http://www.angularjs.org上当前的文档不够详尽,但它确实可以帮助您入门。尽管如此,我将对上传的示例项目提供一些简要的思考/解释。
demo.js:这是包含我们的视图模型或控制器的JavaScript文件。请注意,完全相同的视图模型可以根据标记的不同而产生不同的行为。这是一个相当简单的示例,包含一个项目集合、一个标题以及指向活动项目的指针。如果您正确使用Angular,您的ViewModel
将完全独立于UI。DOM操作、标记创建等不应该出现在您的ViewModel
中。
directives:您的自定义属性应该尽可能通用。它们是DOM和HTML相关工作的合适之处,并且它们应该独立于任何ViewModel
,以保持这些属性完全模块化和可重用。本文将重点关注directives.js,稍后将在文章中详细介绍。
HTML文件:为了清晰起见,我遵循了一些约定。任何以“ng-
”开头的属性都表示它是一个调用Angular功能的属性,这是Angular本身使用的约定。为了防止与现有或未来的Angular属性发生冲突,我选择将所有自定义指令都以“ng-ds-
”开头。我将所有指令或属性驱动的行为保存在文件Directives.js中。这使您能够构建一个属性库,以非常模块化和强大的方式在整个项目中使用。
jquery:其中一个示例使用了jquery模态框。除此之外,我在指令中使用了基本的jquery。使用您喜欢的任何其他模态框都是微不足道的。您可能会发现自己在ViewModel
中使用jquery进行Ajax调用,但这并不是必需的,因为Angular提供了自己的Ajax实用工具。(但这超出了本文的范围。)请注意,您可以在没有jquery的情况下轻松使用Angular。
创建属性/指令
Directives.js必须在angular.js之后包含。这样您就可以声明自己的Angular模块。
var dsApp = angular.module('dsApp', []);
我对此没有深入研究,但第一个属性是与您分配给ng-app
属性的值匹配的名称。第二个是依赖项数组(超出了本文的范围,也是我尚未深入研究的内容)。变量名显然不必与属性中的名称匹配。
创建模块后,您然后在模块上声明每个指令。这些指令将成为Angular将处理和连接的属性。那么让我们看看我们的第一个指令。
dsApp.directive('ngDsFade', function () {
return function (scope, element, attrs) {
element.css('display', 'none');
scope.$watch(attrs.ngDsFade, function (value) {
if (value) {
element.fadeIn(200);
} else {
element.fadeOut(100);
}
});
}
});
当属性中评估的值为true
时,此指令将使元素淡入,当值为false
时,它将淡出。因此,您应该在两个示例的HTML中看到ng-ds-fade
在多个地方使用。
第一个参数定义了属性。这里的参数名称应该是驼峰式命名。然后Angular会将其转换为用连字符分隔的小写属性。所以ngDsFade => ng-ds-fade
作为一个您可以使用的属性。第二个参数是一个函数,它返回Angular用来连接您的属性行为的代码。您返回的函数在viewmodel
(作用域)、带有属性的dom节点(元素)以及此模块拥有的任何其他属性(attrs)上进行评估。
您可以做的第一件事是设置一些初始化。在这种情况下,我们隐藏元素进行初始化。值得注意的是,在代码的这一点上,属性出现在dom元素上的顺序很重要。任何位于当前属性(本例中为ng-ds-fade
)之后的属性将不会出现在attrs中。因此,如果您需要使用多个属性,如果您在设置监视器大部分时间之外进行操作,则顺序变得很重要。当监视器评估时,所有属性都已存在。所以大多数情况下,这不是问题。我不鼓励创建属性顺序成为功能要求的代码。这显然会导致难以维护或模块化的代码。
scope.$watch
是力量所在。它设置了当属性中的值改变时,Angular将评估该函数。在这种情况下,true
或非null
的值将使用jquery的淡入效果淡入,false
或null
的值将淡出。
示例中另一个同样简单的指令/属性是ng-ds-active
。它只是根据绑定到属性的值在元素上添加或删除活动类。这是另一个简单但非常有用的属性,您可以在任何地方创建和重用。
模态指令
好的,那么让我们看一个更有趣的。如何制作一个可以打开或关闭模态框,并在模态框中显示DOM元素内容的东西。因此,只需在标记中放入属性ng-ds-modal="EditMode"
(其中EditMode
是您ViewModel
上的一个布尔值),您就可以通过简单地更改ViewModel
上EditMode
的值来控制模态框。有趣的是,您可以在属性值中执行一些基本的布尔语法,并且监视器会很好地处理它。例如,在模态示例HTML中,我们设置了ng-ds-modal="ActiveItem!=null"
。好的,现在让我们看看这个指令的JavaScript。
dsApp.directive('ngDsModal', function () {
return function (scope, element, attrs) {
var diag = $(element).dialog(
{autoOpen:false,
close:function()
{
try
{
var functionName= attrs.ngDsModalClosed;
//alert(functionName);
if(typeof scope[functionName]=="function")
setTimeout(function(){scope.$apply(scope[functionName]);},100);
}
catch(err)
{}
}
});
scope.$watch(attrs.ngDsModal, function (value) {
if (value) {
$(element).dialog("open");
}
else
{
$(element).dialog("close");
}
});
}
});
这里的初始化代码从传入的dom节点设置了一个jquery对话框。作为其中一部分,我们设置了关闭事件处理程序。当该函数评估时,attrs
对象包含所有属性,因此属性顺序无关紧要。请注意,close
函数会查找函数名。如果我们在作用域上定义了一个函数,那么我们就调用它。这里需要两个技巧。
scope.$apply
是必要的,以便执行的函数能够导致Angular重新评估绑定值。如果您不通过scope.$apply
调用,那么对您的ViewModel
的任何更改都不会导致绑定更新反映在DOM等上。- 我通过艰难的方式(以及一些Google搜索)发现,这里需要一个超时,以防止Angular中出现错误。大概是关于摘要已经在进行中或类似的事情。我不记得具体细节,也不假装完全理解为什么会出现错误。
所以我们需要关闭模态处理程序,因为可以通过jquery的关闭按钮关闭jquery模态框,而这个按钮没有以任何方式绑定到我们的视图模型。换句话说,我们希望有一种方法让jquery关闭的模态框通知ViewModel
它已经关闭了,以便ViewModel
可以更新任何它应该更新的属性。
我们为ViewModel
设置了一个处理程序,它将清除ActiveItem
指针。请注意,我们如何使用第二个属性(ng-ds-modal-closed
)来定义模态框关闭时要调用的函数。这使我们能够对任何ViewModel
使用相同的指令,甚至可以将其用于一个ViewModel
中不同元素上的多个属性。
内联编辑指令
好的,如果我解释得足够清楚,您已经理解了,那么我们可以继续讨论一个更重要、更复杂的。我想要一个能让我们定义一个模板用于内联编辑的指令。为了实现这一点,我们设置了三个属性,并在几个地方使用。
内联模板
通过将此属性放在模板标记(我们在另一个示例中放入模态框的内容)上,我们对该元素调用了一些初始化。此属性不需要任何监视器,因此它只设置了一些非常简单的初始化。现在让我们来看看。
dsApp.directive('ngDsInlineTemplate',function(){
return function(scope,element,attrs)
{
var templateElement = $(element);
var name= templateElement.attr("ng-ds-inline-template");
$(element).hide();//hide our template.
scope[name]=element;//save a pointer to the template node in the
}
});
我们首先获取元素并保存其名称。我没有在attrs
数组中找到该值,但通过简单地检查模板上的属性,我能够获取到它。我们隐藏了模板。然后,我们在ViewModel
上保存一个同名属性。这使我们能够在需要时在单个ViewModel
上拥有多个内联编辑模板。因此,此属性和指令的目的只是为我们提供一个指向模板的指针。它保存在作用域上,但实际上ViewModel
从不使用它。我们在另一个指令中使用它。
InlineEdit 和 TemplateName
ng-ds-template-name
设置了 ds-inline-edit
用于查找模板中使用的 DOM 元素的名称。
dsApp.directive('ngDsInlineEdit',function()
{
return function(scope,element,attrs)
{
//$(element).hide();//hide our template.
scope.$watch(attrs.ngDsInlineEdit,function(value)
{
//we need to get our template element to move it and show, or just hide it.
if(!attrs.ngDsTemplateName)
return;//we don't have a template yet.
var template = $(scope[attrs.ngDsTemplateName]);
if(value)
{
setTimeout(function(){
//move the template here and show it.
$(element).after(template);
template.fadeIn(200);
},10);
}
else
{
template.hide();
}
});
}
});
这个感觉和模态框的例子是反向的。我们根据项目决定模板放在哪里。在模态框中,我们将通过监视器评估的属性放在模态框的内容上。在这个例子中,我们将放置和显示模板的条件放在特定项目上。所以当特定项目为true
时,模板会立即放置在该元素之后。这里的等待实际上是正确处理切换活动项目所必需的。否则,我们可能会遇到时间问题,即我们移动它,然后隐藏完成,内联编辑不可见。当为false
时,它被隐藏。
因此,要使这组指令工作,您需要以下属性
ng-ds-inline-template='somename'
:放置在作为您的内联编辑模板的DOM节点上ng-ds-template-name='somename'
:放置在您希望当该项目处于编辑模式时将该模板移动到的任何项目上ng-ds-inline-edit=condition
:当条件评估为true
时,结果是将模板显示在您放置此属性的项目之后。
限制
此内联编辑指令是一个互斥模型。也就是说,它一次只能在一个地方使用模板。做一个允许您编辑多个项目的版本将是一个有趣且并不十分困难的练习。但这也将要求您的ViewModel
了解多个活动项目。
我也意识到示例标记在技术上并不正确。通过在无序列表中放置一个div
,我有点淘气了。内联编辑示例可能最适用于表格。或者我可以稍微重新排列标记,使编辑模板的位置在li
标签内,而不是它的同级。然而,在我偷懒的示例中,那样会使所有内容都加粗。同样,这些都是足够简单的事情来补救,但对于本文的目的来说并不关键。
结论
我希望这能让您了解通过创建自己的指令/属性,Angular可以实现一些有用而强大的功能。当您正确操作时,您最终会得到可以在整个项目中使用的行为。您可以看到相同的ViewModel
如何在不修改的情况下用于各种行为和体验。标记几乎保持相同,即使考虑到指令行为的JavaScript,事情也保持非常简单。
致谢
Tony Spencer(博客网址即将推出)在帮助我开始创建自定义属性方面功不可没。在Angular生命周期的这个阶段,社区还没有那么丰富,示例也很难找到。Tony在这个示例项目中创建了淡入和活动功能,并回答了我很多问题。