学习 AngularJS 自定义指令:实践方法






4.98/5 (13投票s)
在本文中,我们将了解什么是指令,然后我将尝试解释如何构建自定义指令。
引言
在我上一篇关于 AngularJS 的文章中,我试图解释 AngularJS 的基础知识,以便您入门。在本文中,我将解释 AngularJS 的核心概念,即指令。首先,我们将了解什么是指令,然后我将尝试通过我为大家准备的一些示例来解释如何使用它们构建自定义指令。此外,在本文中,我将更多地关注实际示例和代码,而不是通过冗长生硬的定义和专业术语让您感到困惑。
指令
AngularJS 中的指令是 HTML 元素的属性,AngularJS 为我们提供了这些属性来增强 HTML 元素的功能。AngularJS 提供了许多内置指令,例如 ng-app、ng-controller、ng-repeat、ng-model、ng-bind 等等。因此,通过在 HTML 标签上使用 ng-app,我们标志着 AngularJS 应用的开始。AngularJS 还提供了一个功能,允许我们编写自己的自定义指令,以根据我们的需求扩展 HTML 元素的功能。我们可以扩展 HTML 模板的能力,以实现我们所能想象的任何功能。
为什么要使用指令
指令帮助我们创建可重用组件,这些组件可以在整个 Angular 应用程序中使用。
指令语法
让我们来看一个定义指令的基本伪代码。
angularModuleName.directive('directiveName', function() { return { restrict: String, priority: Number, terminal: Boolean, template: String or Template Function, templateUrl: String, replace: Boolean or String, scope: Boolean or Object, transclude: Boolean, controller: String or function(scope, element, attrs, transclude, otherInjectables) { ... }, controllerAs: String, require: String, link: function(scope, iElement, iAttrs) { ... }, compile: return an Object OR function(tElement, tAttrs, transclude) { return { pre: function(scope, iElement, iAttrs, controller) { ... }, post: function(scope, iElement, iAttrs, controller) { ... } } // or return function postLink(...) { ... } } }; });
基本上,指令返回一个具有键值对的定义对象。这些键值对决定了指令的行为。好吧,语法可能看起来令人生畏,但实际上并非如此,因为大多数键都是可选的,有些是互斥的。让我们尝试在实际生活中理解这些键值对的含义。
- Restrict (限制)
用于指定指令将在 DOM 中如何使用。指令可以用作属性(A)、元素(E)、类(C)或注释(M)。这些选项可以单独使用或组合使用。可选表示如果您不指定该选项,则默认值为属性。
示例
作为属性:
<div custom-directive/>
作为元素:
<custom-directive></custom-directive>
作为类:
<div class="custom-directive"/>
作为注释:
<!--custom-directive-->
- 优先级
它指定了 AngularJS 调用指令的优先级。优先级高的指令将比优先级低的指令先被调用。默认值为 0。
- 终结符
好吧,我从未使用过此选项,但文档说明它用于告知 Angular 停止在具有比此指令更高优先级的元素上调用任何其他指令。此选项是可选的。
- 模板
它指定了指令的内联 HTML 模板。
- TemplateUrl (模板 URL)
您还可以使用包含模板代码的文件的 URL 来加载指令的模板。如果使用了内联模板,则不使用此选项。
- 替换
它可以设置为 true 或 false。指令的模板将附加到使用该指令的父元素。如果此选项设置为 false,或者如果设置为 true,它将替换父元素。
- 范围
它指定了指令的范围。这是一个非常重要的选项,应根据您正在构建的指令的需求非常谨慎地设置。指令的范围有三种设置方式:
- 来自指令 DOM 元素的现有范围。如果范围设置为 false(这是默认值),则指令的范围将与指令所用 DOM 元素的范围相同。
- 继承自封闭控制器范围的新范围。如果范围设置为 true,则为指令创建一个新范围,该范围将继承父 DOM 元素范围的所有属性,或者我们可以说它将继承指令所封闭 DOM 的控制器范围。
- 一个独立范围,它不会继承任何来自其父项的内容。这种类型的范围通常用于制作可重用组件。
目前,上面的陈述和要点可能没有太大意义,除非您看到所有选项都在实际中应用。所以别担心,我将在接下来的部分通过示例来解释所有要点。
- Transclude (转接)
正如我之前解释过的,指令可以替换或追加其内容/模板到父 DOM 元素,但使用 Transclude 选项,您也可以将原始内容移动到指令的模板内,前提是 Transclude 设置为 true。
- 控制器 (Controller)
您可以使用此选项指定要为指令使用的控制器名称或控制器函数。
- ControllerAs (控制器别名)
可以使用该选项为控制器指定替代名称。
- Require (必需)
它指定了要使用的另一个指令的名称。如果您的指令依赖于另一个指令,则可以使用此选项指定相同指令的名称。
- 链接
它用于定义一个函数,该函数可用于以编程方式修改模板 DOM 元素实例,例如添加事件监听器、设置数据绑定等。
- 编译
它可用于定义一个函数,该函数可用于以编程方式修改 DOM 模板,以实现指令副本之间的功能。它还可以返回链接函数以修改生成的元素实例。
示例 1:创建第一个指令
让我们使用上面的语法编写我们的第一个指令。
var ngCustomDirectivesApp = angular.module('ngCustomDirectivesApp') ngCustomDirectivesApp.directive('customLabel', function () { return { restrict: 'EA', template: '<div class="jumbotron"><h2>My First Directive</h2><p>This is a simple example.</p></div>' } })
解释
与其他 Angular 应用一样,我首先创建了一个 Angular 模块,然后定义了一个指令函数,它简单地返回一个具有两个属性的对象:“restrict
”(限制)和“template
”(模板)。如您所见,对于 restrict
属性,我使用了 EA,这意味着该指令既可以用作 DOM 中的元素,也可以用作属性。Template
属性包含一个字符串,我在其中定义了指令的结构。该指令将渲染一个包含标题和段落的 div。这里需要注意的一点是指令的命名约定,正如您所见,我遵循了驼峰命名法,因为当 Angular 解析此指令时,它会用连字符分割名称。所以 customLabel
将变成 custom-label,在我们的 HTML 模板中,我们将不得不使用 custom-label 名称。您可能已经注意到,AngularJS 的所有预定义指令都带有 ng- 前缀。在 DOM 中,指令将用作
<custom-label></custom-label>
或者
<div custom-label></div>
输出
注意:我为 div 使用了 Bootstrap 的 jumbotron
类,因为我已将 Bootstrap 包含在应用程序中。
示例 2:使用 Link 函数
在这个示例中,我将创建一个简单的“喜欢按钮”指令,它将使用其链接函数以编程方式将点击事件绑定到指令实例元素上以执行某些操作。
指令定义
ngCustomDirectivesApp.directive('likeButton', function () { return { restrict: 'EA', templateUrl: '../partials/likebutton.html', replace: true, link: function (scope, element, attrs) { element.bind('click', function () { element.toggleClass('like'); }) } } })
模板
<button type="button" class="btn-sty"> <span class="glyphicon glyphicon-thumbs-up"></span> </button>
CSS
.like { background-color: blue; color: white; } .btn-sty { border-radius: 12px; }
输出
解释
与前面的示例一样,此指令的定义相同,不同的是在这个指令中,我没有编写内联模板,而是在单独的文件中编写了指令的模板。让我们尝试理解链接函数。如您所见,链接函数有三个参数:第一个是 scope,通过它可以访问指令实例元素的范围;第二个是 element,通过它可以访问指令的元素,在这个例子中,我将按钮作为一个元素来访问;第三个是 attr,它是指令元素的属性。现在回到我们的代码,在链接函数中,我获取了元素并绑定了点击事件,在这个事件中我只是切换了类。
示例 3:理解作用域
在这个示例中,我将尝试解释指令的作用域。所有指令都有一个与之关联的作用域,用于访问我在上一个示例中提到的模板和链接函数中的方法和数据。除非明确指定,否则指令不会创建自己的作用域,而是将其父作用域用作自己的作用域。正如我之前解释过的,scope 属性的值决定了作用域如何在指令内部创建和使用。scope 属性有三个不同的值可以设置。这些值可以是 true、false 或 {}。
Scope: false (作用域:false)
当作用域设置为 false 时,指令将使用其父作用域作为自己的作用域,这意味着它可以访问和修改父作用域的所有数据/变量。如果父级在其作用域中修改其数据,则更改将反映在指令作用域中。同样,如果指令尝试修改父作用域的数据,也会发生这种情况,因为父级和指令访问同一作用域,它们都可以看到彼此的更改。
指令定义
ngCustomDirectivesApp.controller('dashboardController', function ($scope) { $scope.dataFromParent = "Data from parent"; }) ngCustomDirectivesApp.directive('parentScope', function () { return { restrict: 'E', scope: false, template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>' } }) <div ng-controller="dashboardController"> <input type="text" ng-model="dataFromParent" style="border:1px solid green" /> <parent-scope></parent-scope> </div>
输出
解释
首先,我创建了一个控制器并定义了一个作用域变量 dataFromParent
。接下来,我创建了一个指令并将作用域设置为 false。在模板中,我简单地创建了一个输入框,该输入框通过 ng-model
绑定到 dataFromParent
。然后,我创建了一个父 div,其控制器与我在第一步中定义的控制器相同。在此 div 中,我创建了一个输入框,它也绑定到 dataFromParent
,然后我在同一个 div 中使用了该指令,因此控制器作用域将充当指令的父作用域。现在,如果您更改任何一个输入框的值,更改都会反映在另一个上,因为这两个输入框都从同一个控制器访问相同的 dataFromParent
。简而言之,当作用域设置为 false 时,控制器和指令使用相同的共享作用域对象。因此,对控制器和指令的任何更改都会保持同步。
Scope: true (作用域:true)
当作用域设置为 true 时,会创建一个新的作用域并分配给指令,并且该作用域对象将从其父作用域对象那里原型继承。因此,在这种情况下,对这个新作用域对象的任何更改都不会反映回父作用域对象。但是,因为新作用域是从父作用域继承的,所以父作用域中的任何更改都会反映在指令作用域中。
ngCustomDirectivesApp.controller('dashboardController', function ($scope) { $scope.dataFromParent = "Data from parent"; }) ngCustomDirectivesApp.directive('inheritedScope', function () { return { restrict: 'E', scope: true, template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>' } }) <div ng-controller="dashboardController"> <input type="text" ng-model="dataFromParent" style="border:1px solid green" /> <parent-scope></parent-scope> <inherited-scope></inherited-scope> </div>
输出
解释
就像上一个指令一样,我定义了一个新指令。但在该指令中,我将作用域设置为 true,这意味着在这种情况下,指令将不再访问父作用域对象(控制器作用域),而是为其自身创建一个新的作用域对象(但它将从父作用域继承)。因此,当第一个文本框发生任何更改时,所有其他文本框也将更新,但如果第三个文本框(即我们的指令)发生任何更改,则更改不会反映在前两个文本框中。前两个文本框直接从控制器访问数据,而第三个文本框由于原型继承而访问其新作用域中的数据。
Scope : { } (作用域:{})
当作用域设置为对象字面量 {} 时,会为指令创建一个新的作用域对象。但这一次,它不会从父作用域对象继承,它将完全与其父作用域分离。此作用域也称为隔离作用域。创建这种类型的指令的优点是它们是通用的,可以在应用程序中的任何位置放置,而不会污染父作用域。
ngCustomDirectivesApp.controller('dashboardController', function ($scope) { $scope.dataFromParent = "Data from parent"; }) ngCustomDirectivesApp.directive('isolatedScope', function () { return { restrict: 'E', scope: {}, template: '<input type="text" ng-model="dataFromParent" style="border:1px solid red;"/>' } }) <div ng-controller="dashboardController"> <input type="text" ng-model="dataFromParent" style="border:1px solid green" /> <parent-scope></parent-scope> <inherited-scope></inherited-scope> <isolated-scope></isolated-scope> </div>
输出
解释
在这种情况下,会创建一个新的作用域,该作用域无法访问其父作用域对象,因此数据不会被绑定。
示例 4:理解隔离作用域
正如我之前所说,如果您将指令的作用域设置为对象字面量 {},则会为指令创建一个隔离作用域,这意味着指令无法访问父作用域对象的数据/变量或方法。这对于创建可重用组件可能非常有用,但在大多数情况下,我们需要指令与父作用域之间进行某种通信,并且还希望指令不会污染父作用域。因此,隔离作用域提供了一些过滤器来在父作用域对象和指令作用域对象之间进行通信或交换数据。要将一些数据从父作用域传递到指令作用域,我们需要向我们设置给 scope 属性的对象字面量添加一些属性。让我们先看看语法,然后我再解释它们。
scope: { varibaleName1:'@attrName1', varibaleName2:'=attrName2', varibaleName3:'&attrName3' }
或,
scope: { attrName1:'@', attrName2:'=', attrName3:'&' }
在隔离作用域中,有三个选项可用于将数据从父级传递到指令。
@
: 文本绑定或单向绑定或只读访问。这是指令和父作用域之间的一种单向绑定,它期望属性被映射为一个表达式({{ }}
)或字符串。由于它提供单向绑定,因此父作用域中的更改将反映在指令作用域中,但指令作用域中的任何更改都不会反映回父作用域。
=
: 模型绑定或双向绑定。这是父作用域和指令之间的双向绑定,它期望属性值为模型名称。父作用域和指令作用域之间的更改将同步。
&
: 方法绑定或行为绑定。它用于将父作用域中的任何方法绑定到指令作用域,因此它提供了在父作用域中执行任何回调的优势。
示例
让我们创建一个简单的指令来理解所有作用域选项的用法。首先,创建一个将作为指令父级的控制器。在控制器中,定义一个名为 dataFromParent
的作用域变量和一个名为 changeValue
的函数来修改该变量。
ngCustomDirectivesApp.controller('dashboardController', function ($scope) { $scope.dataFromParent = "Data from parent"; $scope.changeValue = function () { $scope.dataFromParent = "Changed data from parent"; } })
现在,让我们创建我们的指令。
ngCustomDirectivesApp.directive('isolatedScopeWithFilters', function () { return { restrict: 'E', scope: { oneWayBindData: '@oneWayAttr', twoWayBindData: '=twoWayAttr', methodBind:'&parentMethodName' }, template: '<input type="text" ng-model="oneWayBindData" style="border:1px solid red;"/><br/><input type="text" ng-model="twoWayBindData" style="border:1px solid red;"/><br/><button type="button" ng-click="methodBind()">Change Value</button>' } })
如您在 scope 中所见,我添加了三个属性。这些属性将在我们的指令中用于绑定数据。指令非常简单,我们创建了两个文本框和一个按钮。第一个文本框绑定到 scope 的 oneWayData
属性,第二个文本框绑定到 twoWayData
属性,按钮的 ng-click 事件绑定到 methodBind
属性。请仔细查看作用域属性中使用的前缀。
让我们在一个 div 中使用这个指令,将其控制器设置为我们在第一步中定义的控制器。现在在这里添加我们的指令,指令元素将具有三个属性,名为 one-way-attr、two-way-attr 和 parent-method-name,这些属性与我们在指令定义中使用驼峰命名法不同,它们是以连字符分隔的,符合 Angular 语法。此外,添加一个段落标签并使用表达式将其值映射到 dataFromParent
,以便我们看到 dataFromParent
模型的值的实时更新。
<div ng-controller="dashboardController"> <isolated-scope-with-filters one-way-attr="{{ dataFromParent}}" two-way-attr="dataFromParent" parent-method-name="changeValue()"></isolated-scope-with-filters> <p>{{dataFromParent}}</p> </div>
one-way-attr
映射到表达式,该表达式将评估父作用域(即我们的控制器)中的 dataFromParent
模型的值;two-way-attr 直接映射到 dataFromParent
模型;parent-method-attr 映射到控制器中的函数以更改模型的值。
运行代码,亲自看看指令是否正常工作。
示例 5:与控制器一起工作
让我们创建一个示例来理解控制器如何用于不同指令之间的通信。在此示例中,我们将创建一个手风琴(accordion)指令。
步骤 1:创建父级手风琴指令,它将包含子级手风琴元素。
ngCustomDirectivesApp.directive('accordion', function () { return { restrict: 'EA', template: '<div ng-transclude></div>', replace: true, transclude: true, controllerAs: 'accordionController', controller: function () { var children = []; this.OpenMeHideAll = function (selectedchild) { angular.forEach(children, function (child) { if (selectedchild != child) { child.show = false; } }); }; this.addChild = function (child) { children.push(child); } } } })
在模板中,我们定义了一个普通 div,但需要注意的重要一点是,我们使用了 ng-transclude,ng-transclude 将使 div 能够包含子元素。出于允许 div 包含子元素的相同原因,Transclude 选项设置为 true。然后定义了一个控制器,这是该指令的重点区域。在控制器中,定义一个函数来将子元素推送到数组中,然后定义一个函数来打开选定的子级并隐藏所有其他子级。
步骤 2:创建手风琴子元素指令。
ngCustomDirectivesApp.directive('accordionChild', function () { return { restrict: 'EA', template: '<div><div class="heading" ng-click="toggle()">{{title}}</div><div class="content" ng-show="show" ng-transclude></div></div>', replace: true, transclude: true, require: '^accordion', scope: { title:'@' }, link: function (scope,element,attrs,accordionController) { scope.show = false; accordionController.addChild(scope); scope.toggle = function () { scope.show = !scope.show; accordionController.OpenMeHideAll(scope); } } } })
手风琴元素将有一个标题和一个主体来容纳数据或其他元素,因此在模板中我们将创建一个 head div 并附加一个点击事件进行切换,然后我们必须创建一个 body div 来容纳内容,为了能够容纳动态内容,我们必须在此 div 中使用 ng-transclude。Require 属性用于指定该指令需要手风琴指令。ng-show 用于在单击 head div 事件时隐藏和显示内容。创建了隔离作用域以使其成为可重用组件,并且 title 属性用于单向数据绑定。在链接函数中,作用域用于访问 show 模型,该模型将用于显示和隐藏内容,并且将手风琴指令的控制器传递给访问其方法。
当用户单击手风琴元素的标题时,会将该元素的引用传递给手风琴控制器中的函数,以显示特定元素并隐藏所有其他元素。
步骤 3:在视图中使用指令。
<accordion> <accordion-child title="Element 1">Data 1</accordion-child> <accordion-child title="Element 2">Data 2</accordion-child> <accordion-child title="Element 3">Data 3</accordion-child> </accordion>
输出
示例 6:与控制器一起工作
让我们创建另一个使用控制器指令的示例。在此示例中,我们将创建一个可排序列表。列表元素可以拖动以根据我们的需要对列表项进行排序。
步骤 1:定义一个指令,根据传递给它的项目数组来创建列表。
ngCustomDirectivesApp.directive('smartList', function () { return { restrict: 'EA', templateUrl: '../partials/listdirective.html', replace: true, scope: { items: '=' }, controller: function ($scope) { $scope.source = null; }, controllerAs:'listCTRL' } })
模板
<ul class="ul-sty"> <li ng-repeat="item in items" class="li-sty" draggable> {{item }} </li> </ul>
指令非常简单,它包含一个无序列表,其列表项由 ng-repeat 生成。我们还添加了一个 draggable 属性,它将使列表项可拖动。我们将在接下来的步骤中定义 draggable 指令。在指令的 scope 属性中,我们使用了 items 进行双向数据绑定,这意味着我们将能够在指令作用域中访问父作用域的模型。我们还定义了一个控制器,其中包含一个名为 source 的变量。
步骤 2:创建可拖动指令。
ngCustomDirectivesApp.directive('draggable', function () { return { require:'^smartList', link: function (scope, element, attr, listCTRL) { element.css({ cursor: 'move', }); attr.$set('draggable', true); function isBefore(x, y) { if (x.parentNode == y.parentNode) { for (var i = x; i; i = i.previousSibling) { if (i == y) return true; } } return false; } element.on('dragenter', function (e) { if (e.target.parentNode != null) { if (isBefore(listCTRL.source, e.target)) { e.target.parentNode.insertBefore(listCTRL.source, e.target) } else { e.target.parentNode.insertBefore(listCTRL.source, e.target.nextSibling) } } }) element.on('dragstart', function (e) { listCTRL.source = element[0]; e.dataTransfer.effectAllowed = 'move'; }) } } })
此可拖动指令用于我们在第 1 步中定义的模板中。在该指令的链接函数中,我们将第 1 步中定义的指令的控制器传递了进去。它将元素的属性设置为 draggable,定义了一个函数来比较传递元素的父节点。在元素上附加了 drag enter 事件来处理拖放。我们将被拖动的元素存储在控制器变量中,以便与当前元素被放置的元素进行比较。除此之外,其余代码非常简单,我们只是将节点插入到合适的位置。
步骤 3:定义父控制器。
ngCustomDirectivesApp.controller('dashboardController', function ($scope) { $scope.itemsdata = ['Apple', 'Mango', 'Banana', 'PineApple', 'Grapes', 'Oranges']; })
步骤 4:使用指令
<div ng-controller="dashboardController"> <smart-list items="itemsdata" /> </div>
在我们的指令中,来自控制器的 itemsdata 被传递到指令的作用域。
输出
您可以拖动项目以根据需要对其进行排序。
结论
我尝试通过示例涵盖指令的所有方面。指令可以在项目的各种场景中使用。您可以在 AngularJS 官方文档中了解更多关于指令的信息。如果您想精通指令,那么最好的方法就是开始在我们的项目中创建指令。我已将完整代码附在文章中。您也可以从 GitHub 下载或克隆它(https://github.com/vikas0sharma/Custom-Directive-Examples)。我希望它能帮助您理解 AngularJS 中的指令。