AngularJS 1.x 小记(二)

AngularJS 1.x 小记(二)

指令(Directive)

指令是AngularJS中另一个主要的特性,指令的作用可以用一句话描述,就是可以给HTML元素赋予特殊或自定义的行为,比如监听事件、视图模板代理等。在上文中我们使用过的 ng-app 、 ng-controller 、 ng-model 就是AngularJS中的指令。

指令的命名

指令的命名和使用写法比较有意思,一般情况下在定义指令时推荐使用驼峰命名法,比如 ngModel 、 ngApp ,但是在HTML中大小写是不敏感的,所以在HTML中使用指令时推荐使用小写字母加破折号的形式,比如 ng-model 、 ng-app 。除了使用小写破折号这种方式,还有以下几种使用写法:

  • ng:model
  • ng_model
  • data-ng-bind
  • x-ng-bind

大家可以根据自己喜好选择使用写法,但是尽量保持写法统一。

指令的形式

在AngularJS中,指令有四种表现形式,既标签形式、标签属性形式、标签class名称形式、注释形式:

<my-dir></my-dir>
<span my-dir="exp"></span>
<!-- directive: my-dir exp -->
<span class="my-dir: exp;"></span>

一般情况下,推荐使用标签形式和标签属性形式。

使用指令

与Controller一样,指令也是通过AngularJS的Model创建的,使用 directive(name, directiveFactory) 方法创建指令,该方法有两个参数:

  • 第一个参数为指令名称,命名规范在上文中已经说过了。
  • 第二个参数是一个工厂函数,该函数需要返回一个对象,我们通过配置该对象中的不同属性从而告诉AngularJS内置的 $compile 服务实现指令的不同功能。

指令类型

上文中说过指令的实现是通过工厂函数返回对象,然后通过配置对象的不同属性实现不同功能,所以设置指令的类型也是通过配置属性对象完成的:

var mainModule = angular.module("mainModule", []);

mainModule.directive("myDirective", function() {
  return {
    restrict: "A"
  };
});

mainModule.directive("myDirective1", function() {
  return {
    restrict: "E"
  };
});

mainModule.directive("myDirective2", function() {
  return {
    restrict: "AE"
  };
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Directive</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div my-directive></div>
    <my-directive1></my-directive1>
    <my-directive2></my-directive2>
    <!-- <div my-directive2></div> -->
  </body>
</html>

从上面的示例代码可以看出,可以通过配置返回对象的 restrict 属性设置指令的类型,可配置的值分别为:

  • "A" :代表且仅可代表标签属性类型的指令。
  • "E" :代表且仅可代表标签类型的指令。
  • "C" :代表且仅可代表class名称类型的指令。
  • "M" :代表且仅可代表注释类型的指令。

如果想设置一个多类型的指令,那么可以将类型标识写在一起,比如 "AEC" 代表既是标签属性类型,又是标签类型,还是class名称类型。如果不配置 restrict 属性,那么表示指令的类型为默认的 "AE" 类型。

通过指令封装UI模板

在前端应用的开发过程中,不同的页面常有很多一样的UI元素,如果每个页面都写一遍,那么在维护时就常会牵一发而动全身,AngularJS中的指令可以很好的解决这个问题,它可以将UI片段封装为一个指令,从而可以在不同的页面中复用,那么在维护时就是四两拨千斤的效果。下面来看看如何实现模板指令:

var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function() {
  this.name = "Jason";
  this.job = "Developer";
});

mainModule.directive("myDirective", function() {
  return {
    template: "Name: {{mc.name}}, Job: {{mc.job}}"
  };
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Directive</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="MyController as mc">
      <my-directive></my-directive>
      <!-- <div my-directive></div> -->
    </div>
  </body>
</html>

上面的代码示例中,我们将本该写在HTML中的展示代码设置给了返回对象的 template 属性。为了能更好的管理UI模板,我们还可以将UI展示代码提炼成单独的HTML模板文件,并可以使用指令的 templateUrl 属性设置HTML模板文件名称,这样便可以大大降低指令和UI模板的耦合度:

mainModule.directive("myDirective", function() {
  return {
    templateUrl: "myTemplate.html"
  };
});
<!-- myTemplate.html -->
Name: {{mc.name}}, Job: {{mc.job}}

那么问题来了,如果UI模板文件很多的话,按上面的方法就要写很多个对应的指令,而且只是UI模板文件名称不一样而已,实在有点冗余。AngularJS提供了另外一种解决方法,那就是可以通过给模板指令设置相关属性,从而动态的加载UI模板文件,我们来看看如何实现:

// modules.js
var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function() {
  this.name = "Jason";
  this.job = "Developer";
});

mainModule.directive("myDirective", function() {
  return {
    templateUrl: function(elem, attr) {
      return "myTemplate-" + attr.type + ".html";
    }
  };
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Directive</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="MyController as mc">
      <my-directive type="name"></my-directive>
      <my-directive type="job"></my-directive>
      <!-- <div my-directive type="name"></div> -->
      <!-- <div my-directive type="job"></div> -->
    </div>
  </body>
</html>
<!-- myTemplate-name.html -->
Name: {{mc.name}}

<!-- myTemplate-job.html -->
Job: {{mc.job}}

从上面的示例代码可以看出, template 和 templateUrl 两个属性的值不只是接受字符串,还接受函数。要注意的是该函数默认带两个参数:

  • 第一个参数代表当前的HTML DOM元素。
  • 第二个参数代表当前HTML DOM元素的属性对象,在函数体内可以为该对象设置任何属性。

在上面的示例中,我们给代表当前DOM元素的属性对象设置了 type 属性,用于标识UI模板文件名称,这样我们就可以通过一个专有的模板指令来控制所有的UI模板文件了。

指令的作用域

上面的示例中,我们通过配置可以实现动态加载UI模板文件,但是我们无法动态指定UI模板文件中显示的内容。这一节我们来了解一下如何通过指令的隔离域达到在同一个指令中动态指定UI模板文件中要显示的内容,先看看代码示例:

var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function() {
  this.jason = { name: "Jason", job: "Developer" };
  this.green = { name: "Green", job: "Doctor" };
});

mainModule.directive("myDirective", function() {
  return {
    restrict: "E",
    scope: {
      personInfo: "=person"
    },
    templateUrl: "myTemplate.html"
  };
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Directive</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="MyController as mc">
      <my-directive person="mc.jason"></my-directive>
      <hr>
      <my-directive person="mc.green"></my-directive>
    </div>
  </body>
</html>
<!-- myTemplate.html -->
Name: {{personInfo.name}}, Job: {{personInfo.job}}

从示例中可以看到,我们给指令的返回对象又添加了一个属性 scope ,这就是指令的作用域属性, scope 属性有三种可设置的值:

  • false :默认值,这表示指令共享它父节点的Controller的作用域,也就是可以使用双花括号语法直接访问父节点Controller作用域中的属性。
  • true :创建指令自己的作用域,但是该作用域继承父节点Controller的作用域。
  • {} :第三种是设置一个对象,表示创建了指令自己独立的作用域,与父节点Controller的作用是完全隔离的。

如果我们希望指令的隔离作用域和父节点Controller的作用域之间进行交互,那么就需要将两者进行绑定,这里有三种绑定方式:

  • 使用 @ 实现单向数据绑定,但是只限于绑定Controller作用域中值为字符串的属性,因为是单向绑定,所以父节点Controller修改绑定的属性可影响到指令作用域中对应的属性,反之则不可以。在HTML中使用双花括号语法取值,比如 person=""。
  • 使用 = 实现双向数据绑定,在父节点Controller中修改属性和在指令中修改属性可相互影响。在HTML中直接使用属性名称,比如 person="jasonObj" 。
  • 使用 & 实现函数绑定,用于绑定Controller中值为函数的属性,在HTML中直接调用函数,比如 action="click()" 。

上面的示例中我们给 myDirective 指令设置了隔离域并添加了名为 personInfo 的属性,并与父节点的 MyController 进行数据双向绑定,在HTML代码中,就可以通过<my-directive> 指令标签的 person 属性与 MyController 的数据绑定了。另外,在进行绑定时还有一种简写的方式:

...
scope: {
  personInfo: "="
  // personInfo: "@"
  // personInfo: "&"
},
...

等同于:

...
scope: {
  personInfo: "=personInfo"
  // personInfo: "@personInfo"
  // personInfo: "&personInfo"
},
...

指令的Controller

在指令中也可以创建Controller,和在Module中创建Controller很类似,既定义函数,在参数中注入需要的AngularJS服务既可:

var mainModule = angular.module("mainModule", []);

mainModule.controller("MyController", function($scope) {
  $scope.green = { name: "Green", job: "Doctor" };
});

mainModule.directive("myDirective", function() {
  return {
    restrict: "E",
    scope: {
      person: "="
    },
    controller: function($scope) {
      $scope.jason = { name: "Jason", job: "Developer" };
    },
    templateUrl: "myTemplate.html"
  };
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Directive</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div ng-controller="MyController">
      <my-directive person="green"></my-directive>
    </div>
  </body>
</html>
<!-- myTemplate.html -->
Name: {{jason.name}}, Job: {{jason.job}}
Name: {{person.name}}, Job: {{person.job}}

在上面的示例中,我们给 myDirective 指令添加了Controller,有一点不同的是在添加Controller时不能设置名称,指令的Controller名称默认与指令名称一样,如果需要另外指定名称,需要配置 controllerAs 指定Controller的名称:

...
controller: function($scope) {
  $scope.jason = { name: "Jason", job: "Developer" };
},
controllerAs: "directiveController",
...

在上面示例的UI模板文件中可以看出,既可以使用指令隔离域中与父节点Controller绑定的属性,也可以使用在指令自己的Controller中定义在隔离域的属性。

指令之间的交互

指令之间的交互主要是以指令的Controller为桥梁来实现的,这里的交互指的是子指令与父指令之间的交互,我们可以使用指令的 require 属性设置要引用的父指令的Controller,这里有几种配置方式:

  • require: "controllerName" :只查找指令自己的Controller。
  • require: "^controllerName" :查找指令自己的Controller以及父指令的Controller。
  • require: "^^controllerName" :只查找父指令的Controller。
  • require: ["^controllerName1", "^controllerName2"] :引用多个Controller。

如果指令查找到引用的Controller后该如何使用呢,这就要使用指令的另一个重要的属性 link 函数了。 link 函数主要用来为DOM元素添加事件监听、监视模型属性变化、以及更新DOM,该函数共有五个参数:

  • scope :指令的作用域,默认是父节点Controller的作用域,如果指令有创建自己的作用域,那么则指指令自己的作用域。
  • element :指令的jQLite(jQuery的子集)包装的DOM元素,可以通过该参数操作指令所在的DOM元素。
  • attrs :指令所在DOM元素的属性对象,通过 . 语法可以获取到给DOM元素添加的属性。
  • controller :指令通过 require 属性引用的Controller实例。
  • transcludeFn :嵌入函数。

link 函数的其他几个参数后面文章中都会讲到,当指令找到通过 require 属性引用的Controller后,我们就可以通过 link 函数的第四个参数访问引用的Controller了。来看一个示例:

通过指令操作DOM元素

我们了解了 link 函数后就可以使用该函数实现各种有用的指令了,比如通过指令操作DOM元素:

var mainModule = angular.module("mainModule", []);

mainModule.directive("myDirective", function($interval) {
  return {
    restrict: "A",
    link: function(scope, element, attrs) {
      $interval(function() {
        element.text(new Date());
      }, 1000);
    }
  };
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Directive</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <div>
      Current Date is: <span my-directive></span>
    </div>
  </body>
</html>

上面的示例中,首先我们限定了 myDirective 指令只能以标签属性的形式使用,然后注入了AngularJS的内置服务 $interval ,通过 link 函数的第二个参数获取到指令所在的DOM元素,然后周期性更新DOM元素显示的内容。

AjmUji3

指令的内嵌机制

大家都知道HTML中的DOM元素是具有层级关系的,一般情况下我们使用指令封装的UI模板颗粒度都会比较小,所以就会出现指令嵌套的现象,这几需要用到指令的内嵌机制了,指令的 transclude 属性默认为 false ,如果将其设置为 true ,那么该指令就开启了内嵌机制,也就是说指令标签之间的内容可以被指定嵌入UI模板中被 ng-transclude 内置指令标记过的DOM元素中,结合之前说过的父子指令交互的内容来实现一个例子:

// modules.js
var mainModule = angular.module("mainModule", []);

mainModule.directive("myTabs", function() {
  return {
    restrict: "E",
    transclude: true,
    controller: function($scope) {
      $scope.panes = [];
      var panes = $scope.panes;
      this.addPane = function(pane) {
        if(panes.length == 0) {
          $scope.select(pane);
        };
        panes.push(pane);
      };
      $scope.select = function(pane) {
        angular.forEach(panes, function(pane) {
          pane.selected = false;
        });
        pane.selected = true;
      };
    },
    templateUrl: "myTabs.html"
  };
});

mainModule.directive("myPane", function() {
  return {
    restrict: "E",
    require: "^^myTabs",
    scope: {
      name: "@",
      job: "@"
    },
    link: function(scope, element, attrs, controller) {
      controller.addPane(scope);
    },
    templateUrl: "myPane.html"
  };
})
<!--index.html-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demo for Directive</title>
    <script src="../angular-1.5.8.js"></script>
    <script src="modules.js"></script>
  </head>
  <body ng-app="mainModule">
    <my-tabs>
      <my-pane name="Jason" job="Developer"></my-pane>
      <my-pane name="Green" job="Doctor"></my-pane>
    </my-tabs>
  </body>
</html>

<!--myTabs.html-->
<div>
  <ul>
    <li ng-repeat="pane in panes">
      <a href="" ng-click="select(pane)">{{pane.name}}</a>
    </li>
  </ul>
  <div id="paneContainer" ng-transclude></div>
</div>

<!--myPane.html-->
<div ng-show="selected">
  I am {{name}}, my job is {{job}}!
</div>

在上面的示例中,我们创建了两个指令, myTabs 和 myPane ,在 myTabs 指令中,我们限定它只能以标签形式使用,开启了内嵌机制,并定义了它自己的Controller,在Controller中定义了 panes 变量和 addPane(pane) , select(pane) 两个方法,方法的具体实现内容这里就不解释了,都很简单,最后指定了UI模板文件 myTabs.html 。

在 myPane 指令中同样限定只能以标签形式使用,指定了要引用的父节点的Controller名称,后创建了自己的隔离域,定义了 name , job 两个属性,并进行了字符串的单向绑定,然后定义了 link 函数,通过第四个参数访问到了父节点的 myTabs Controller,并调用 addPane(pane) 函数,将自己的隔离域作为参数传入,最后指定了UI模板文件 myPane.html 。

再来看看 index.html 和 myTabs.html , myPane.html 这两个模板文件:

在 index.html 中, myTabs 指令包含两个 myPane 指令,这两个 myPane 指令所显示的内容将嵌入在 myTabs.html 中id为 paneContainer 的DIV中,也就是myPane.html 中的内容会被嵌入在这个DIV里。

上面这三个文件中有几个点需要注意:

  • 因为在 myPane 指令的隔离域中定义了 name 和 job 属性,并进行了字符串绑定,所以在 index.html 文件中,可以对 myPane 标签里的 name , job 属性直接进行字符串赋值。
  • 因为在 myPane 指令中引用了 myTabs 指令的Controller,并在 link 函数中将隔离域作为参数传给了 myTabs ,既 myTabs 指令的Controller中的 select(pane) 和 addPane(pane) 函数的参数均为 myPane 指令的隔离域,所以在 myTabs.html 文件中可以直接使用 pane 访问 myPane 指令隔离域中定义的属性,比如 ,并且也可以在 myTabs 指令在 myPane 的隔离域中定义属性,比如 pane.selected = true,给隔离域定义了 selected 的属性,然后可以在 myPane 指令中使用。
  • ng-show 是AngularJS内置的指令,用于显示或隐藏指定的DOM元素。

看看运行效果:

qei22mR

link 函数的第五个参数 transcludeFn 是一个函数,该函数常用的有两个参数 scope 和 function(clone){} ,既 transcludeFn(scope, function(clone){}) 。前者是嵌入内容的作用域,与指令的隔离作用域是平行的,后者函数的参数 clone 是嵌入的内容的jquery封装,可以通过它对嵌入的内容进行DOM操作。

指令的其他属性

priority用于指定指令的优先级,该属性的值从1开始。当有多个指令定义在同一个DOM元素中时就需要通过该属性明确它们的执行顺序。

replace用于判定是否将UI模板的内容替换掉指令标签本身,该属性默认值为 false,既保留指令标签本身,若设置为 true 则替换指令标签。


本文出处:http://www.tuicool.com/articles/reqAFn


最后编辑于 2016-12-22 21:59