65.9K
CodeProject 正在变化。 阅读更多。
Home

创建 AngularJS 自定义指令 - 第 I 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.61/5 (12投票s)

2014年11月10日

CPOL

11分钟阅读

viewsIcon

52838

downloadIcon

1105

创建 AngularJS 自定义指令。

引言

AngularJS 框架为客户端 Web 开发者提供了丰富的功能和特性。从我开始研究这个框架的那天起,我就被其深度和广度所折服。该框架试图将一些我们过去在服务器端习以为常或认为更容易开发的特性迁移到客户端开发环境中。虽然之前已经存在客户端 JavaScript UI 组件,甚至像 JQuery 这样的库可以加速客户端开发,但 AngularJS 通过重新构想 HTML 和 JavaScript 的交互方式,为客户端开发带来了全新的视角。

这意味着,作为开发者,我们需要改变思考问题域及其解决方案的方式。起初,这种思维方式可能比较困难,因为我们大多数人都是在扎实的服务器端开发背景下成长起来的,但当我们开始欣赏 AngularJS 带来的优势时,很快就能适应。

该框架中一个特别令人感兴趣且不可忽视的功能是——指令,尤其是自定义指令以及如何创建它们。

本文(第一部分)将介绍创建非常简单的自定义指令所需的步骤。相关 Fiddle 可以在这里找到。

本部分将尝试介绍以下内容

  • 指令定义对象 (DDO) 的基本工作原理和选项 – 指令的结构以及使其工作的内部机制。
  • 页面与指令之间的数据共享/数据绑定。
  • 在指令的父级执行表达式或函数。

注意:要继续阅读,您需要对 AngularJS 及其内置指令有基本了解。

那么,我们开始吧

首先,我们在 Visual Studio 中创建一个名为 StarRating-Directive 的新项目。项目模板将是 ASP.NET Empty Web Application。这里使用 Visual Studio 是可选的,因为我们不会用到任何 .NET 功能。代码提供了 Visual Studio 和非 Visual Studio 版本。

在新建的项目中,我们创建以下文件夹结构

/StarRating-Directive

      /app

           /directives

           /controllers

           /views

      /Images

      /Scripts

      /Style 

app 文件夹将包含我们的 AngularJS 应用程序,其中的子文件夹包含应用程序的不同组件。
Scripts 文件夹将包含 AngularJS 库文件 - angular.jsangular.min.js
Images 文件夹将包含我们指令使用的图片 - star-empty-lg.pngstar-fill-lg.png
最后,在 Style 文件夹中,我复制了 Bootstrap 的分发文件。
完成后,我们的解决方案资源管理器将是这样的

现在我们的项目骨架已经创建好了,让我们开始我们的应用程序。

代码

应用程序

我们在 app 文件夹的根目录下添加了一个新文件 app.js。该文件将包含我们的应用程序模块(‘app’),所有其他应用程序组件都将绑定到该模块。

/// <reference path="../Scripts/angular.js" />

'use strict';
var app = angular.module('app', []);

应用程序中的所有 .js 文件都使用 <reference path=[file path] /> 标签来获得 JavaScript 的 IntelliSense 支持,相关介绍请参见这里

我们将控制器添加到模块中 – 在 controllers 文件夹中添加一个新文件 appController.js。起初,控制器将在其 $scope 上有一个名为 starRating 的属性,其值为 3

/// <reference path="../Scripts/angular.js" />
/// <reference path="../app.js" />

'use strict';
app.controller('appController', ['$scope', function ($scope) {
    $scope.starRating = 3;
}]); 

指令

指令在模块上定义,语法如下,并返回一个定义指令选项的对象字面量。因此,返回的对象被称为指令定义对象 (DDO)。

app.directive('starRating', function () {
    return {
       . . .
});

至少,指令定义对象需要定义以下选项…

template: 以 HTML 定义的用户界面,将显示为指令。作为替代,对于复杂的用户界面,可以将 template 属性替换为 templateUrl templateUrl 指向一个 .html 文件。

restrict: 值可以是以下任何一种或全部

E: 指令定义为元素。<star-Rating rating=”rating”></star-Rating>
A: 指令作为属性应用于现有元素。<div star-rating rating=”rating”></div>
C: 指令作为 CSS 类应用于现有元素 <div class="star-rating" rating="rating "></div>
M: 指令作为注释应用。

controller: 控制器是视图和作用域(作用域是指令的模型)之间的连接。作为指令的控制器,它控制指令的功能,并负责更新内部 DOM 元素以及与外部世界进行通信。

好了,让我们根据迄今为止学到的知识构建我们的指令,并为您的 StarRating 指令创建一个极简化的指令定义。

app.directive('starRating', function () {
    return {        
        restrict: 'EA',
        template:
            "<div style='display: inline-block; margin: 0px; 
            padding: 0px; cursor:pointer' ng-repeat='idx in maxRatings track by $index'> \
                <img ng-src='{{(rating <= $index) && 
                \"Images/star-empty-lg.png\" || \"Images/star-fill-lg.png\"}}' \
                ng-Click='click($index + 1)'></img> \
            </div>",
        controller: function ($scope) {
            $scope.maxRatings = [];

            for (var i = 1; i <= 5; i++) {
                $scope.maxRatings.push({});
            };

            $scope.click = function (param) {
                $scope.rating = param;
            };
        }
    };
});

上面的指令有一个 restrict 选项,值为 ‘EA’,这意味着该指令可以应用于 Element Attribute

指令的 template 包含一个 div ,其中包含一个 ng-repeat 指令,循环遍历 $scope 上定义的 maxRatings 数组。因此,模板在用户界面上生成五个星形。img ng-src 根据评分设置为空星或实心星。

ng-src 指令的计算方式为:ng-src = {{ condition && true || false}} :其中 condition 检查 rating 是否小于或等于 ng-repeat $index

img 标签上还有一个 ng-click 指令,它调用 $scope 上的 click() 函数。

最后是控制器,它将负责我们指令的功能,并且将为我们指令的每个实例执行。

在控制器中,我们创建了一个大小为 5 的数组 $scope.maxRatings 。我们需要这个数组来使 ng-repeat 正常工作。我们 $scope 上的 click() 函数也在这里创建,当点击 img 时会被调用,从而改变 $scope.rating

StarRating 指令的用法如下

<div class="container" 
ng-app="app" ng-controller="appController">
    <div>
        <div class="alert alert-success"> 
        <span class="label label-info">Star Rating: {{rating}}</span>
            <star-rating rating></star-rating>
        </div>
    </div>
</div>

渲染效果如下

在这里,我们可以看到我们的指令正在工作。它以我们上面在页面控制器中定义的评分 3 进行初始化。我们也可以通过点击星形来更改评分。我们可以看到评分的变化,它显示在指令的左侧,证实了指令与外部世界之间的双向绑定正如预期那样工作。最棒的是,我们用一行代码就实现了这一点。

<star-rating rating></star-rating>

现在,让我们将指令的另一个副本添加到页面上,如这里所示。

正如我们在 Fiddle 中看到的,当我们更改一个指令的评分时,另一个也会随之改变。

问题

  1. 我们使用了页面控制器(父作用域)中的 Rating,我们的指令中有。目前 没有办法将两个不同的 Rating,即 Rating1 Rating2 传递给我们的指令副本。
  2. 两个指令副本都在共享父作用域上 Rating 变量的同一个副本。
  3. 当一个指令更改其评分时,它会更新父作用域中的值——由于父作用域与指令存在双向绑定,这也会导致另一个指令更改其评分。

解决方案:为我们的指令提供一个私有作用域,以便每个指令实例都可以拥有一个私有作用域副本,该副本可以从父级初始化。在 AngularJS 中,这个私有作用域称为隔离作用域。

注意:在某些情况下,我们可以做一个例外,在指令中使用父作用域。例如,用户名指令。每当登录用户更改时,用户名指令在其使用的任何地方都会反映出这种变化。

隔离作用域

隔离作用域的类型是通过将以下值之一传递给 scope 参数来设置的。

= 在本地作用域属性和父作用域属性之间设置双向绑定。
@ 将本地作用域属性绑定到 DOM 属性的 string 值。此值也是单向的,并且是从父作用域传递到隔离作用域。
& 提供了一种在父作用域的上下文中执行表达式的方法。传递到 DOM 中此属性的值始终被期望为一个函数或表达式。

注意:定义作用域属性还有其他选项,上面未提及,但可以在此处找到。

双向绑定

在指令中添加隔离作用域的方法是向指令定义对象添加一个 scope 选项。

scope: {
     rating: '='
}

这里的 scope 选项通过一个属性 – rating 来创建,其值为 ‘=’。

这意味着,隔离作用域有一个名为 rating 的属性,其值 ‘=’ 表示它将与同名的父作用域 ‘rating’ 进行双向对象绑定。

让我们来看看实际效果。在此处创建的 Fiddle 中展示了工作示例。

在 Fiddle 中,当我们添加上述 scope 选项 - { } 到指令时,我们对页面控制器进行了以下更改。

app.controller('appController', ['$scope', function ($scope) {
    $scope.rating1 = 2;
    $scope.rating2 = 3;
}]);

我们对页面标记进行了以下更改。

<div class="container" 
ng-app="app" ng-controller="appController">
    <div>
        <div class="alert alert-success"> 
        <span class="label label-info">Star Rating: {{rating1}}</span>
            <star-rating rating='rating1'></star-rating>
        </div>
        <div class="alert alert-info"> 
        <span class="label label-info">Star Rating: {{rating2}}</span>
            <star-rating rating='rating2'></star-rating>
        </div>
    </div>
</div>

在页面控制器中,我们在其作用域上创建了两个属性 rating1 rating2 ,并将它们传递到我们指令标记中的 rating 属性,如下所示

<star-rating rating='rating1'></star-rating>
<star-rating rating='rating2'></star-rating>

DOM 中的 rating 属性代表了页面控制器和指令的隔离作用域之间的桥梁。传递到属性的值通过这个桥梁传递给指令,并且通信是双向的。

所以现在我们有了两个工作的指令,它们可以独立共存,并且可以按预期拥有和更改它们的评分。

单向绑定

在某些情况下,您可能需要将数据从父级发送到指令的作用域,而不期望该数据返回。对于这种情况,我们可以在指令的隔离作用域上定义单向作用域属性。

我们当前的 starRating 指令最多显示五个星形。如果我们想让星形的数量动态化怎么办?我们可以向指令的作用域添加一个 maxRating 属性。当然,指令不会从内部更改此属性!

这样的属性可以通过给 scope 属性设置 ‘@’ 值来定义。这个 scope 属性期望从 DOM 属性获取一个 string 值,并且与父作用域没有双向绑定。

scope: {
    rating: '=',
    maxRating: '@',
}

这使得我们改变了指令控制器中初始化 $scope.maxRatings 数组的方式。现在数组不再是固定大小为五,而是动态的,并且基于从父级传递的 maxRating 值。

$scope.maxRatings = [];

for (var i = 1; i <= $scope.maxRating; i++) {
    $scope.maxRatings.push({});
};

从指令调用并执行父级上的表达式或方法

隔离作用域的最后一个强大功能是可以执行父作用域上的表达式或方法。这非常方便且至关重要,当指令需要执行父级上的某些代码时。
例如,我们将在每次点击星形时调用父级上的一个函数,并将更改后的评分值传递给父级。
这样的属性可以通过给 scope 属性设置 ‘&’ 值来定义。这个 scope 属性期望从 DOM 属性获取一个函数或表达式。
指令中更改后的隔离作用域将是

scope: {
    rating: '=',
    maxRating: '@',
    click: '&',
}

指令的模板将更改为使用 ng-click 指令来监听点击事件,并调用 isolatedClick() 函数,该函数是指令本地的,它又调用 DOM 中声明的父函数,从而将点击事件广播到指令外部。

"<div style='display: inline-block; margin: 0px; 
padding: 0px; cursor:pointer;' ng-repeat='idx in maxRatings track by $index'> \
    <img ng-src='{{(rating <= $index) && \"Images/star-empty-lg.png\" 
    || \"Images/star-fill-lg.png\"}}' ng-Click='isolatedClick($index + 1)'></img> \
</div>"

指令中接收首次点击通知的函数将如下所示,它将调用绑定到其隔离作用域的父级上的 click 函数。

$scope.isolatedClick = function (param) {
    $scope.rating = param;
    $scope.click({ param: param });
};

在我们的页面上,指令现在看起来像这样,其中 click 属性绑定到指令作用域同名的属性。属性的值是一个函数 click1(param),当指令调用其隔离的 click 函数时将被调用。

<div star-rating rating="starRating1" click="click1(param)"></div>

页面控制器包含 click1 函数,目前只是记录评分值,但显然可以用于更多用途!

$scope.click1 = function (param) {
    console.log('Click(' + param + ')');
};

感到困惑?相信我,一开始这确实令人困惑。希望下面的工作流程能帮助您更好地理解。

我向指令的隔离作用域添加了三个新属性

  • readOnly:一个单向属性,添加到作用域以使指令变为只读。
  • mouseHover:一个函数,将在父级上执行,就像上面描述的 click 函数一样,这次告诉父级哪个星形当前鼠标悬停。
  • mouseLeave:一个函数,将在父级上执行,通知父级哪个星形已经失去了鼠标悬停。
scope: {
    rating: '=',
    maxRating: '@',
    readOnly: '@',
    click: "&",
    mouseHover: "&",
    mouseLeave: "&"
}

隔离作用域的类型的工作原理已在上面讨论过,为保持文章简短,此处不再解释新添加的作用域属性!
完整的可工作指令可以在这个 Fiddle 这里找到。

结论

我们在这里创建了一个相当简单的指令。

从代码中可以看出,整个指令是用大约 60 行代码创建的,并包含在一个可移植的文件中,该文件包括 HTML 模板、数据和指令的行为。指令的结构类似于服务器端类似的尝试,例如带有 public 属性和事件的用户控件。

用 JQuery 创建类似的东西肯定会更复杂,而且最终用户可以用一行标记来使用指令,这本身就很惊人,可以节省创建和测试此类组件的时间和精力。

在下一部分,我们将看到 DDO 的更多选项,并尝试创建一个更复杂的指令!

© . All rights reserved.