本文转自https://www.w3ctech.com/topic/1612

这系列的上一篇文章,我讨论了scope事件以及digest循环的行为。这一次,我将谈论指令。这篇文章包括 独立的scope,内嵌,link函数,编译器,指令控制器等等。

如果这个图表看起来非常的费解,那么这篇文章很适合你。

AngularJS系统学习之Directive(指令)

(Image credit: Angular JS documentation) (Large version)

声明: 这篇文字是基于 AngularJS v1.3.0 tree.

Link

AngularJS中,指令是 通常是小的 组件, 这意味着跟DOM交互。他经常被用作顶层DOM的抽象层,大多数的操作可以不用jQuery,jqLite等包装的DOM元素。通过使用表达式、其他的指令来得到你想要的结果是高明的。

在AngularJS的核心里,指令可以绑定元素的属性(例如可见性,class列表,内部文本,内部HTML或者值)到scope的属性或表达式。最值得注意的是,一旦监测到scope中的变化被标记,这些绑定就会被更新。反过来也是相似的,使用$observe函数能够监测DOM属性,当监测到属性变化时会触发一个回调。

简单的说,指令是AngularJS中很重要的一面。如果你精通指令,那么处理AngularJS程序你将不会有任何问题。同理,如果你不设法理解指令,你将很难将其用在合适的地方。熟练指令需要时间,尤其是你在尝试不仅仅是用jQuery封装代码就完事。

在AngularJS,你能够建立组件化的指令、服务和控制器,它们可以复用,只要复用是合理的。例如你有一个简单的指令,基于一个监测的scope表达式来切换class ,在你的代码中用来标识你的特定组件的状态,我想那是一个十分通用的指令,能够在你的程序里到处使用到。你可以有一个服务集成键盘快捷键的服务、控制器、指令和其他注册快捷键的服务,它支持所有键盘快捷键的处理在一个自包含的服务中。

指令也是可重用的功能,但经常被分配给 DOM 片段,或者模板,而不是仅仅提供功能。是时候深入了解 AngularJS 指令及其他的用法了。

Link

之前,我列出了 AngularJS 中 scope 上可用的属性,我用他来解释 digest 机制以及 scope 如何操作的。 我会用同样的方式来解释指令, 但是这次我将剖析指令的工厂函数返回的对象的属性,以及每个这些属性如何影响我们所定义的指令。

首先要注意下指令的名字, 来看一个简单的例子。

angular.module('PonyDeli').directive('pieceOfFood', function () {
  var definition = { //

尽管在上面的的代码片段中我们定义了一个命名为'pieceOfFood'的指令,AngularJS约定在 HTML 标记里使用破折号的形式连接名字。如果这个指令作为一个属性实现,那么我在 HTML 中就会像这样调用:

<span piece-of-food></span>

默认情况,指令只能作为属性被触发。但是如果你想改变这种方式,你可以使用 restrict 属性。

如何定义一个指令作为标签使用。

angular.module('PonyDeli').directive('pieceOfFood', function () {
  return {
    restrict: 'E',
    template: // ...
  };
});

出于某种原因,我无法捉摸它们决定混淆什么,本来一个很有表达能力的框架,却以单个大写字母结尾来定义指令是如何被限制的。GitHub 上有一个可用的restrict选项列表, restrict 的默认值是 EA

  • 'A': attributes are allowed
  • 'A': 允许作为一个属性
    <span piece-of-food></span>
  • 'E': elements are allowed
  • 'E': 允许作为一个元素
    <piece-of-food></piece-of-food>
  • 'C': as a class name
  • 'C': 作为一个类名
    <span class='piece-of-food'></span>
  • 'M': as a comment
  • 'M': 作为一个注释
    <!-- directive: piece-of-food -->
  • 'AE': You can combine any of these to loosen up the restriction a bit.
  • 'AE': 可以结合上面的任意值来放松限制。

千万别用 'C' 或者 'M' 来限制你的指令。 用 'C' 不能使之在标记中凸显出来, 用 'M' 是为了向后兼容。 如果你觉得有趣, 你可以用一个例子来设置 restrict 为 'ACME'。

不幸的是, 指令定义对象的其他属性是很难理解的。

如何设置一个指令与父级 scope 交互。

因为我们在之前的文章中大范围的谈论了 scope,知道如何正确地使用 scope 属性,所以不应为此感到痛苦。我们从默认值开始, scope: false,使作用域链保持不受影响:依照我在上篇文章提到的规则你将得到与元素相关联的所有作用域。

当你的指令不会和 scope有互动,保持作用域链不变显然是有用的,但这种情况很少发生。一种更常见有用的情景是,不改变作用域创建一个指令,给他一个作用域,实例化多次并且只跟一个scope属性交互———指令的名字。跟默认值restrict: 'A'结合是最有表达力的。(下面的代码在 Codepen上可用)

angular.module('PonyDeli').directive('pieceOfFood', function () {
  return {
    template: '{{pieceOfFood}}',
    link: function (scope, element, attrs) {
      attrs.$observe('pieceOfFood', function (value) {
        scope.pieceOfFood = value;
      });
    }
  };
});
<body ng-app='PonyDeli'> 
  <span piece-of-food='Fish & Chips'></span>
</body>

这里有几个值得注意的点我们还没讨论到。你将在后面的章节了解到 link 属性。 暂且想一下作为一个控制器如何操作每个实例化的指令。

在指令的链接函数里,我们可以获得元素上的属性集合。这个集合有一个特殊的方法,叫$observe(), 当一个属性变化时可以触发一个回调。没有监听属性变化时,属性永远不会对应到scope上,也无法绑定到我们的模板上。

我们可以改下上面的代码,通过引进scope.$eval,让他更可用。记得他是如何依靠scope被用来解析一个表达式的吗?看下面的代码帮助我们更好的理解(也可以查看 Codepen )。

var deli = angular.module('PonyDeli', []);

deli.controller('foodCtrl', function ($scope) {
  $scope.piece = 'Fish & Chips';
});

deli.directive('pieceOfFood', function () {
  return {
    template: '{{pieceOfFood}}',
    link: function (scope, element, attrs) {
      attrs.$observe('pieceOfFood', function (value) {
        scope.pieceOfFood = scope.$eval(value);
      });
    }
  };
});
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
  <span piece-of-food='piece'></span>
</body>

这个例子中,通过 scope 我解析出了属性的值 piece,这个值定义在controller 中的 $scope.piece。当然,直接使用模板方式如{{piece},但是那样需要你特别注意你想追踪的scope属性。这种方式增加了一点灵活性,但当你想在在所有的指令间共享scope时, 如果你尝试用同样的scope添加多个指令则会导致意外的结果 。

Link

你可以创建一个子作用域来解决这个问题, 他继承自父级的原型。为了创建子作用域, 你仅仅需要声明 scope: true。

var deli = angular.module('PonyDeli', []);

deli.controller('foodCtrl', function ($scope) {
  $scope.pieces = ['Fish & Chips', 'Potato Salad'];
});

deli.directive('pieceOfFood', function () {
  return {
    template: '{{pieceOfFood}}',
    scope: true,
    link: function (scope, element, attrs) {
      attrs.$observe('pieceOfFood', function (value) {
        scope.pieceOfFood = scope.$eval(value);
      });
    }
  };
});
<body ng-app='PonyDeli' ng-controller='foodCtrl'>
  <p piece-of-food='pieces[0]'></p>
  <p piece-of-food='pieces[1]'></p>
</body>

正如你所见,现在我们可以使用指令的多个实例来达到预期的效果,因为每个指令都创建了自己的作用域。 但是,这里有一个局限:一个元素的多个指令都是一个相同的作用域。

注意:如果同一元素的多个指令需要新的作用域,那么只会创建一个作用域。

Link

最后一个选项是用来创建一个本地的,独立的作用域。独立的作用域跟子作用域不同在于前者不是继承自他的父级(但是也可以通过 scope.$parent 访问)。你可以像这样声明一个独立的作用域:scope: {}。你可以添加一些属性到这个对象,用来从父级scope获取数据绑定并且当前作用域也可访问。很像restrict,独立scope的属性简洁但语法复杂,你可以用符号例如:&,@ 和=来定义属性的绑定方式。

你可以省略属性名如果你打算使用你本地scope的属性名。那就是说,pieceOfFood: '=' 是 pieceOfFood: '=pieceOfFood'的简写;他们是相等的。

Link

那么,这些符号是什么意思?下面枚举的例子,可以帮助你破解他们

Link

使用 @ 绑定父级作用域]监测属性的结果。

<body ng-app='PonyDeli' ng-controller='foodCtrl'>
  <p note='You just bought some {{type}}'></p>
</body>
deli.directive('note', function () {
  return {
    template: '{{note}}',
      scope: {
        note: '@'
      }
  };
});

等效于观察属性变化来更新本地scope。当然,用 @ 符号是更多的“AngularJS”。

deli.directive('note', function () {
  return {
    template: '{{note}}',
    scope: {},
    link: function (scope, element, attrs) {
      attrs.$observe('note', function (value) {
        scope.note = value;
      });
    }
  };
});

当指令的选项很复杂时,属性监测器很有用。如果我们想通过改变选项来改变指令的行为,我们自己写代码使用attrs.$observe创建检测,比AngularJS 内部去做更有意义,更快。

这个例子中,仅仅替换了 scope.note = value , 如上面的$observe操作所示,任何你想要添加到$watch监听上都应该这样写。

注意:请记住,当遇到 @时,我们谈论的是观察和属性,而不是绑定到父作用域。

Link

使用 & 提供一个 表达式解析函数 ,他的上下文是父级作用域。

<body ng-app='PonyDeli' ng-controller='foodCtrl'>
  <p note='"You just bought some " + type'></p>
</body>
deli.directive('note', function () {
  return {
    template: '{{note()}}',
    scope: {
      note: '&'
    }
  };
});

下面,我已经在link函数里扼要地实现一个相同的功能 ,这个例子中你看不到 & 。这个比用 @ 要长一点点,因为他 是在属性里解析表达式的,也构建了一个可重用的功能。

deli.directive('note', function ($parse) {
  return {
    template: '{{note()}}',
    scope: {},
    link: function (scope, element, attrs) {
      var parentGet = $parse(attrs.note);

      scope.note = function (locals) {
        return parentGet(scope.$parent, locals);
      };
    }
  };
});

真如我们所见,表达式构造器会生成了一个依赖父级scope的方法。你可以随时执行他,甚至可以监测到输出的变化。这个方法在父级scope应该作为只读的查询对待。这样在两种情况下非常有用,当你需要监听父级scope的变化时,这种情况下你应该在表达式函数 note()上设置一个监听, 本质上就像上面的例子。

另一种情况是, 当你需要访问父级scope方法时会派上用场。假设父级scope有一个方法用来更新一个 table,而你的本地 scope用来显示一个table的行。 如果按钮在子scope里,那么通过使用 & 绑定和使用父级scope的刷新方法是很有用的。这仅仅是个人的例子 —— 你或许更喜欢用事件来处理这类事情, 甚至用某种方式构造你的程序来避免一些复杂的事。

Link

使用 = 设置 本地scope与父级scope间的双向数据绑定。

<body ng-app='PonyDeli' ng-controller='foodCtrl'>
  <button countable='clicks'></button>
  <span>Got {{clicks}} clicks!</span>
</body>
deli.directive('countable', function () {
  return {
    template:
      '' +
        'Click me {{remaining}} more times! ({{count}})' +
      '',
    replace: true,
    scope: {
      count: '=countable'
    },
    link: function (scope, element, attrs) {
      scope.remaining = 10;

      element.bind('click', function () {
        scope.remaining--;
        scope.count++;
        scope.$apply();
      });
    }
  };
});

双向数据绑定比 & 或者 @ 更复杂一点

deli.directive('countable', function ($parse) {
  return {
    template:
      '' +
        'Click me {{remaining}} more times! ({{count}})' +
      '',
    replace: true,
    scope: {},
    link: function (scope, element, attrs) {

      // you're definitely better off just using '&'

      var compare;
      var parentGet = $parse(attrs.countable);
      if (parentGet.literal) {
        compare = angular.equals;
      } else {
        compare = function(a,b) { return a === b; };
      }
      var parentSet = parentGet.assign; // or throw
      var lastValue = scope.count = parentGet(scope.$parent);

      scope.$watch(function () {
        var value = parentGet(scope.$parent);
        if (!compare(value, scope.count)) {
          if (!compare(value, lastValue)) {
            scope.count = value;
          } else {
            parentSet(scope.$parent, value = scope.count);
          }
        }
        return lastValue = value;
      }, null, parentGet.literal);

      // I told you!

      scope.remaining = 10;

      element.bind('click', function 

相关文章:

  • 2021-11-28
  • 2021-10-02
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2021-07-25
  • 2022-12-23
  • 2022-12-23
  • 2021-10-20
  • 2021-12-06
  • 2021-05-20
相关资源
相似解决方案