AngularJS学习笔记

自定义指令directive

这是 ng 最强大的一部分,也是最复杂最让人头疼的部分。

目前我们看到的所谓“模板”系统,只不过是官方实现的几个指令而已。这意味着,通过自定义各种指令,我们不但可以完全定义一套“模板”系统,更可以把 HTML 页面直接打造成为一种 DSL (领域特定语言)。

指令的使用

使用指令时,它的名字可以有多种形式,把指令放在什么地方也有多种选择。

通常,指令的定义名是形如 ngBind 这样的 “camel cased” 形式。在使用时,它的引用名可以是:

  • ng:bind
  • ng_bind
  • ng-bind
  • x-ng-bind
  • data-ng-bind

你可以根据你自己是否有 “HTML validator” 洁癖来选择。

指令可以放在多个地方,它们的作用相同:

  • <span my-dir=”exp”></span> 作为标签的属性
  • <span class=”my-dir: exp;”></span> 作为标签类属性的值
  • <my-dir></my-dir> 作为标签
  • <!– directive: my-dir exp –> 作为注释

这些方式可以使用指令定义中的 restrict 属性来控制。

可以看出,指令即可以作为标签使用,也可以作为属性使用。仔细考虑一下,这在类 XML 的结构当中真算得上是一种神奇的机制。

指令的执行过程

ng 中对指令的解析与执行过程是这样的:

  • 浏览器得到 HTML 字符串内容,解析得到 DOM 结构。
  • ng 引入,把 DOM 结构扔给 $compile 函数处理:
    1、找出 DOM 结构中有变量占位符
    2、匹配找出 DOM 中包含的所有指令引用
    3、把指令关联到 DOM
    4、关联到 DOM 的多个指令按权重排列
    5、执行指令中的 compile 函数(改变 DOM 结构,返回 link 函数)
    6、得到的所有 link 函数组成一个列表作为 $compile 函数的返回
  • 执行 link 函数(连接模板的 scope)。

基本的自定义方法

自定义一个指令可以非常非常的复杂,但是其基本的调用形式,同自定义服务大概是相同的:

如果在 directive 中直接返回一个函数,则这个函数会作为 compile 的返回值,也即是作为 link 函数使用。这里说的 compile 和 link 都是一个指令的组成部分,一个完整的定义应该返回一个对象,这个对象包括了多个属性:

  • name
  • priority
  • terminal
  • scope
  • controller
  • require
  • restrict
  • template
  • templateUrl
  • replace
  • transclude
  • compile
  • link

上面的每一个属性,都可以单独探讨的。

下面是一个完整的基本的指令定义例子:

<code lines>
//失去焦点使用 jQuery 的扩展支持冒泡
app.directive('ngBlur', function($parse){
  return function($scope, $element, $attr){
    var fn = $parse($attr['ngBlur']);
    $element.on('focusout', function(event){
      fn($scope, {$event: event});
    });
  }
});
</code>

<div code lines>
//失去焦点使用 jQuery 的扩展支持冒泡
app.directive('ngBlur', function($parse){
  return function($scope, $element, $attr){
    var fn = $parse($attr['ngBlur']);
    $element.on('focusout', function(event){
      fn($scope, {$event: event});
    });
  }
});
</div>
var app = angular.module('Demo', [], angular.noop);

app.directive('code', function(){
  var func = function($scope, $element, $attrs){

    var html = $element.text();
    var lines = html.split('\n');

    //处理首尾空白
    if(lines[0] == ''){lines = lines.slice(1, lines.length - 1)}
    if(lines[lines.length-1] == ''){lines = lines.slice(0, lines.length - 1)}

    $element.empty();

    //处理外框
    (function(){
      $element.css('clear', 'both');
      $element.css('display', 'block');
      $element.css('line-height', '20px');
      $element.css('height', '200px');
    })();

    //是否显示行号的选项
    if('lines' in $attrs){
      //处理行号
      (function(){
        var div = $('
' .replace('%s', String(lines.length).length * 10)); var s = ''; angular.forEach(lines, function(_, i){ s += '<pre style="margin: 0;">%s</pre>\n'.replace('%s', i + 1); }); div.html(s); $element.append(div); })(); } //处理内容 (function(){ var div = $('
'); var s = ''; angular.forEach(lines, function(l){ s += '%s
\n' .replace('%s', l.replace(/\s/g, ' ')); }); div.html(s); $element.append(div); })(); } return {link: func, restrict: 'AE'}; //以元素或属性的形式使用命令 }); angular.bootstrap(document, ['Demo']);

上面这个自定义的指令,做的事情就是解析节点中的文本内容,然后修改它,再把生成的新内容填充到节点当中去。其间还涉及了节点属性值 lines 的处理。这算是指令中最简单的一种形式。因为它是“一次性使用”,中间没有变量的处理。比如如果节点原来的文本内容是一个变量引用,类似于 {{ code }} ,那上面的代码就不行了。这种情况麻烦得多。后面会讨论。

属性值类型的自定义

官方代码中的 ng-show 等算是我说的这种类型。使用时主要是在节点加添加一个属性值以附加额外的功能。看一个简单的例子:

有颜色的文本

有颜色的文本

我们定义了一个叫 color 的指令,可以指定节点文本的颜色。但是这个例子还无法像 ng-show 那样工作的,这个例子只能渲染一次,然后就无法根据变量来重新改变显示了。要响应变化,我们需要手工使用 scope 的 $watch 来处理:

有颜色的文本

有颜色的文本

Compile的细节

指令的处理过程,是 ng 的 Compile 过程的一部分,它们也是紧密联系的。继续深入指令的定义方法,首先就要对 Compile 的过程做更细致的了解。

前面说过, ng 对页面的处理过程:

  • 浏览器把 HTML 字符串解析成 DOM 结构。
  • ng 把 DOM 结构给 $compile ,返回一个 link 函数。
  • 传入具体的 scope 调用这个 link 函数。
  • 得到处理后的 DOM ,这个 DOM 处理了指令,连接了数据。

$compile 最基本的使用方式:

var link = $compile('

{{ text }}

'); var node = link($scope); console.log(node);

上面的 $compile 和 link 调用时都有额外参数来实现其它功能。先看 link 函数,它形如:

function(scope[, cloneAttachFn]

第二个参数 cloneAttachFn 的作用是,表明是否复制原始节点,及对复制节点需要做的处理,下面这个例子说明了它的作用:

A {{ text }}
B
app.controller('TestCtrl', function($scope, $compile){
  var link = $compile($('#a'));

  //true参数表示新建一个完全隔离的scope,而不是继承的child scope
  var scope = $scope.$new(true);
  scope.text = '12345';

  //var node = link(scope, function(){});
  var node = link(scope);

  $('#b').append(node);
});

cloneAttachFn 对节点的处理是有限制的,你可以添加 class ,但是不能做与数据绑定有关的其它修改(修改了也无效):

app.controller('TestCtrl', function($scope, $compile){
  var link = $compile($('#a'));
  var scope = $scope.$new(true);
  scope.text = '12345';

  var node = link(scope, function(clone_element, scope){
    clone_element.text(clone_element.text() + ' ...'); //无效
    clone_element.text('{{ text2 }}'); //无效
    clone_element.addClass('new_class');
  });

  $('#b').append(node);
});

修改无效的原因是,像 {{ text }} 这种所谓的 Interpolate 在 $compile 中已经被处理过了,生成了相关函数(这里起作用的是 directive 中的一个 postLink 函数),后面执行 link 就是执行了 $compile 生成的这些函数。当然,如果你的文本没有数据变量的引用,那修改是会有效果的。

前面在说自定义指令时说过, link 函数是由 compile 函数返回的,也就像前面说的,应该把改变 DOM 结构的逻辑放在 compile 函数中做。

$compile 还有两个额外的参数:

$compile(element, transclude, maxPriority);

maxPriority 是指令的权重限制,这个容易理解,后面再说。

transclude 是一个函数,这个函数会传递给 compile 期间找到的 directive 的 compile 函数(编译节点的过程中找到了指令,指令的 compile 函数会接受编译时传递的 transclude 函数作为其参数)。

但是在实际使用中,除我们手工在调用 $compile 之外,初始化时的根节点 compile 是不会传递这个参数的。

在我们定义指令时,它的 compile 函数是这个样子的:

function compile(tElement, tAttrs, transclude) { ... }

事实上, transclude 的值,就是 directive 所在的 原始 节点,把原始节点重新做了编译之后得到的 link 函数(需要 directive 定义时使用 transclude 选项),后面会专门演示这个过程。所以,官方文档上也把 transclude 函数描述成 link 函数的样子(如果自定义的指令只用在自己手动 $compile 的环境中,那这个函数的形式是可以随意的):

{function(angular.Scope[, cloneAttachFn]}

所以记住,定义指令时, compile 函数的第三个参数 transclude ,就是一个 link ,装入 scope 执行它你就得到了一个节点。

transclude的细节

transclude 有两方面的东西,一个是使用 $compile 时传入的函数,另一个是定义指令的 compile 函数时接受的一个参数。虽然这里的一出一进本来是相互对应的,但是实际使用中,因为大部分时候不会手动调用 $compile ,所以,在“默认”情况下,指令接受的 transclude 又会是一个比较特殊的函数。

看一个基本的例子:

var app = angular.module('Demo', [], angular.noop);

app.directive('more', function(){
  var func = function(element, attrs, transclude){
    var sum = transclude(1, 2);
    console.log(sum);
    console.log(element);  
  }

  return {compile: func,
          restrict: 'E'};
});

app.controller('TestCtrl', function($scope, $compile, $element){
  var s = '123';
  var link = $compile(s, function(a, b){return a + b});
  var node = link($scope);
  $element.append(node);
});

angular.bootstrap(document, ['Demo']);

我们定义了一个 more 指令,它的 compile 函数的第三个参数,就是我们手工 $compile 时传入的。

如果不是手工 $compile ,而是 ng 初始化时找出的指令,则 transclude 是一个 link 函数(指令定义需要设置 transclude 选项):

123
app.directive('more', function($rootScope, $document){
  var func = function(element, attrs, link){
    var node = link($rootScope);
    node.removeAttr('more'); //不去掉就变死循环了
    $('body', $document).append(node);
  }

  return {compile: func,
          transclude: 'element', // element是节点没,其它值是节点的内容没
          restrict: 'A'};
});

把节点内容作为变量处理的类型

回顾最开始的那个代码显示的例子,那个例子只能处理一次节点内容。如果节点的内容是一个变量的话,需要用另外的思路来考虑。这里我们假设的例子是,定义一个指令 showLenght ,它的作用是在一段文本的开头显示出这段节点文本的长度,节点文本是一个变量。指令使用的形式是:

{{ text }}

从上面的 HTML 代码中,大概清楚 ng 解析它的过程(只看 show-length 那一行):

  • 解析 div 时发现了一个 show-length 的指令。
  • 如果 show-length 指令设置了 transclude 属性,则 div 的节点内容被重新编译,得到的 link 函数作为指令 compile 函数的参数传入。
  • 如果 show-length 指令没有设置 transclude 属性,则继续处理它的子节点( TextNode )。
  • 不管是上面的哪种情况,都会继续处理到 {{ text }} 这段文本。
  • 发现 {{ text }} 是一个 Interpolate ,于是自动在此节点中添加了一个指令,这个指令的 link 函数就是为 scope 添加了一个 $watch ,实现的功能是是当 scope 作 $digest 的时候,就更新节点文本。

与处理 {{ text }} 时添加的指令相同,我们实现 showLength 的思路,也就是:

  • 修改原来的 DOM 结构
  • 为 scope 添加 $watch ,当 $digest 时修改指定节点的文本,其值为指定节点文本的长度。

代码如下:

app.directive('showLength', function($rootScope, $document){
  var func = function(element, attrs, link){

    return function(scope, ielement, iattrs, controller){
      var node = link(scope);
      ielement.append(node);
      var lnode = $('');
      ielement.prepend(lnode);

      scope.$watch(function(scope){
        lnode.text(node.text().length);
      });
    };
  }

  return {compile: func,
          transclude: true, // element是节点没,其它值是节点的内容没
          restrict: 'A'};
});

上面代码中,因为设置了 transclude 属性,我们在 showLength 的 link 函数(就是 return 的那个函数)中,使用 func 的第三个函数来重塑了原来的文本节点,并放在我们需要的位置上。然后,我们添加自己的节点来显示长度值。最后给当前的 scope 添加 $watch ,以更新这个长度值。

指令定义时的参数

指令定义时的参数如下:

  • name
  • priority
  • terminal
  • scope
  • controller
  • require
  • restrict
  • template
  • templateUrl
  • replace
  • transclude
  • compile
  • link

现在我们开始一个一个地吃掉它们……,但是并不是按顺序讲的。

priority
这个值设置指令的权重,默认是 0 。当一个节点中有多个指令存在时,就按着权限从大到小的顺序依次执行它们的 compile 函数。相同权重顺序不定。

terminal
是否以当前指令的权重为结束界限。如果这值设置为 true ,则节点中权重小于当前指令的其它指令不会被执行。相同权重的会执行。

restrict
指令可以以哪些方式被使用,可以同时定义多种方式。

  • E 元素方式 <my-directive></my-directive>
  • A 属性方式 <div my-directive=”exp”> </div>
  • C 类方式 <div class=”my-directive: exp;”></div>
  • M 注释方式 <!– directive: my-directive exp –>

transclude
前面已经讲过基本的用法了。可以是 ‘element’ 或 true 两种值。

compile
基本的定义函数。

function compile(tElement, tAttrs, transclude) { ... }

link
前面介绍过了。大多数时候我们不需要单独定义它。只有 compile 未定义时 link 才会被尝试。

function link(scope, iElement, iAttrs, controller) { ... }

scope
scope 的形式。 false 节点的 scope , true 继承创建一个新的 scope , {} 不继承创建一个新的隔离 scope 。 {@attr: ‘引用节点属性’, =attr: ‘把节点属性值引用成scope属性值’, &attr: ‘把节点属性值包装成函数’}

controller
为指令定义一个 controller ,

function controller($scope, $element, $attrs, $transclude) { ... }

name
指令的 controller 的名字,方便其它指令引用。

require
要引用的其它指令 conroller 的名字, ?name 忽略不存在的错误, ^name 在父级查找。

template
模板内容。

templateUrl
从指定地址获取模板内容。

replace
是否使用模板内容替换掉整个节点, true 替换整个节点, false 替换节点内容。


var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(element, attrs, link){
    console.log('a');
  }

  return {compile: func,
          priority: 1,
          restrict: 'EA'};
});

app.directive('b', function(){
  var func = function(element, attrs, link){
    console.log('b');
  }

  return {compile: func,
          priority: 2,
          //terminal: true,
          restrict: 'A'};
});

上面几个参数值都是比较简单且容易理想的。

再看 scope 这个参数:

var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(element, attrs, link){
    return function(scope){
      console.log(scope);
    }
  }

  return {compile: func,
          scope: true,
          restrict: 'A'};
});

app.directive('b', function(){
  var func = function(element, attrs, link){
    return function(scope){
      console.log(scope);
    }
  }

  return {compile: func,
          restrict: 'A'};
});

app.controller('TestCtrl', function($scope){
  $scope.a = '123';
  console.log($scope);
});

对于 scope :

  • 默认为 false , link 函数接受的 scope 为节点所在的 scope 。
  • 为 true 时,则 link 函数中第一个参数(还有 controller 参数中的 $scope ), scope 是节点所在的 scope 的 child scope ,并且如果节点中有多个指令,则只要其中一个指令是 true 的设置,其它所有指令都会受影响。

这个参数还有其它取值。当其为 {} 时,则 link 接受一个完全隔离(isolate)的 scope ,于 true 的区别就是不会继承其它 scope 的属性。但是这时,这个 scope 的属性却可以有很灵活的定义方式:

@attr 引用节点的属性。

var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(element, attrs, link){
    return function(scope){
      console.log(scope);
    }
  }

  return {compile: func,
          scope: {a: '@abc', b: '@xx', c: '@'},
          restrict: 'A'};
});

app.controller('TestCtrl', function($scope){
  $scope.a = '123';
});
  • @abc 引用 div 节点的 abc 属性。
  • @xx 引用 div 节点的 xx 属性,而 xx 属性又是一个变量绑定,于是 scope 中 b 属性值就和 TestCtrl 的 a 变量绑定在一起了。
  • @ 没有写 attr name ,则默认取自己的值,这里是取 div 的 c 属性。

=attr 相似,只是它把节点的属性值当成节点 scope 的属性名来使用,作用相当于上面例子中的 @xx :

var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(element, attrs, link){
    return function(scope){
      console.log(scope);
    }
  }

  return {compile: func,
          scope: {a: '=abc'},
          restrict: 'A'};
});

app.controller('TestCtrl', function($scope){
  $scope.here = '123';
});

&attr 是包装一个函数出来,这个函数以节点所在的 scope 为上下文。来看一个很爽的例子:

这里
{{ here }}
var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(element, attrs, link){
    return function llink(scope){
      console.log(scope);
      scope.a();
      scope.b();

      scope.show = function(here){
        console.log('Inner, ' + here);
        scope.a({here: 5});
      }
    }
  }

  return {compile: func,
          scope: {a: '&abc', b: '&ngClick'},
          restrict: 'A'};
});

app.controller('TestCtrl', function($scope){
  $scope.here = 123;
  console.log($scope);

  $scope.show = function(here){
    console.log(here);
  }
});

scope.a 是 &abc ,即:

scope.a = function(){here = here + 1}

只是其中的 here 是 TestCtrl 的。

scope.b 是 &ngClick ,即:

scope.b = function(){show(here)}

这里的 show() 和 here 都是 TestCtrl 的,于是上面的代码最开始会在终端输出一个 124 。

当点击“这里”时,这时执行的 show(here) 就是 llink 中定义的那个函数了,与 TestCtrl 无关。但是,其间的 scope.a({here:5}) ,因为 a 执行时是 TestCtrl 的上下文,于是向 a 传递的一个对象,里面的所有属性 TestCtrl 就全收下了,接着执行 here=here+1 ,于是我们会在屏幕上看到 6 。

这里是一个上下文交错的环境,通过 & 这种机制,让指令的 scope 与节点的 scope 发生了互动。真是鬼斧神工的设计。而实现它,只用了几行代码:

case '&': {
  parentGet = $parse(attrs[attrName]);
  scope[scopeName] = function(locals) {
    return parentGet(parentScope, locals);
  }
  break;
}

再看 controller 这个参数。这个参数的作用是提供一个 controller 的构造函数,它会在 compile 函数之后, link 函数之前被执行。

haha
var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(){
    console.log('compile');
    return function(){
      console.log('link');
    }
  }

  var controller = function($scope, $element, $attrs, $transclude){
    console.log('controller');
    console.log($scope);

    var node = $transclude(function(clone_element, scope){
      console.log(clone_element);
      console.log('--');
      console.log(scope);
    });
    console.log(node);
  }

  return {compile: func,
          controller: controller,
          transclude: true,
          restrict: 'E'}
});

controller 的最后一个参数, $transclude ,是一个只接受 cloneAttachFn 作为参数的一个函数。

按官方的说法,这个机制的设计目的是为了让各个指令之间可以互相通信。参考普通节点的处理方式,这里也是处理指令 scope 的合适位置。

kk
var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(){
  }

  var controller = function($scope, $element, $attrs, $transclude){
    console.log('a');
    this.a = 'xx';
  }

  return {compile: func,
          name: 'not_a',
          controller: controller,
          restrict: 'E'}
});

app.directive('b', function(){
  var func = function(){
    return function($scope, $element, $attrs, $controller){
      console.log($controller);
    }
  }

  var controller = function($scope, $element, $attrs, $transclude){
    console.log('b');
  }

  return {compile: func,
          controller: controller,
          require: 'not_a',
          restrict: 'EA'}
});

name 参数在这里可以用以为 controller 重起一个名字,以方便在 require 参数中引用。

require 参数可以带两种前缀(可以同时使用):

  • ? ,如果指定的 controller 不存在,则忽略错误。即:
    require: '?not_b'
    

    如果名为 not_b 的 controller 不存在时,不会直接抛出错误, link 函数中对应的 $controller 为 undefined 。

  • ^ ,同时在父级节点中寻找指定的 controller ,把上面的例子小改一下:
    kk
    

    把 a 的 require 改成(否则就找不到 not_a 这个 controller ):

    require: '?^not_a'
    

还剩下几个模板参数:

  • template 模板内容,这个内容会根据 replace 参数的设置替换节点或只替换节点内容。
  • templateUrl 模板内容,获取方式是异步请求。
  • replace 设置如何处理模板内容。为 true 时为替换掉指令节点,否则只替换到节点内容。

原始内容

var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(){
  }

  return {compile: func,
          template: '

标题 {{ name }}

', //replace: true, //controller: function($scope){$scope.name = 'xxx'}, //scope: {}, scope: true , controller: function($scope){console.log($scope)}, restrict: 'A'} }); app.controller('TestCtrl', function($scope){ $scope.name = '123'; console.log($scope); });

template 中可以包括变量引用的表达式,其 scope 遵寻 scope 参数的作用(可能受继承关系影响)。

templateUrl 是异步请求模板内容,并且是获取到内容之后才开始执行指令的 compile 函数。

最后说一个 compile 这个参数。它除了可以返回一个函数用为 link 函数之外,还可以返回一个对象,这个对象能包括两个成员,一个 pre ,一个 post 。实际上, link 函数是由两部分组成,所谓的 preLink 和 postLink 。区别在于执行顺序,特别是在指令层级嵌套的结构之下, postLink 是在所有的子级指令 link 完成之后才最后执行的。 compile 如果只返回一个函数,则这个函数被作为 postLink 使用:


var app = angular.module('Demo', [], angular.noop);

app.directive('a', function(){
  var func = function(){
    console.log('a compile');
    return {
      pre: function(){console.log('a link pre')},
      post: function(){console.log('a link post')},
    }
  }

  return {compile: func,
          restrict: 'E'}
});

app.directive('b', function(){
  var func = function(){
    console.log('b compile');
    return {
      pre: function(){console.log('b link pre')},
      post: function(){console.log('b link post')},
    }
  }

  return {compile: func,
          restrict: 'E'}
});

Attributes的细节

节点属性被包装之后会传给 compile 和 link 函数。从这个操作中,我们可以得到节点的引用,可以操作节点属性,也可以为节点属性注册侦听事件。


var app = angular.module('Demo', [], angular.noop);

app.directive('test', function(){
  var func = function($element, $attrs){
    console.log($attrs);
  }

  return {compile: func,
          restrict: 'E'}

整个 Attributes 对象是比较简单的,它的成员包括了:

  • $$element 属性所在的节点。
  • $attr 所有的属性值(类型是对象)。
  • $normalize 一个名字标准化的工具函数,可以把 ng-click 变成 ngClick 。
  • $observe 为属性注册侦听器的函数。
  • $set 设置对象属性,及节点属性的工具。

除了上面这些成员,对象的成员还包括所有属性的名字。

先看 $observe 的使用,基本上相当于 $scope 中的 $watch :

var app = angular.module('Demo', [], angular.noop);

app.directive('test', function(){
  var func = function($element, $attrs){
    console.log($attrs);

    $attrs.$observe('a', function(new_v){
      console.log(new_v);
    });
  }

  return {compile: func,
          restrict: 'E'}
});

app.controller('TestCtrl', function($scope){
  $scope.a = 123;
});

$set 方法的定义是:

function(key, value, writeAttr, attrName) { ... } 。
  • key 对象的成员名。
  • value 需要设置的值。
  • writeAttr 是否同时修改 DOM 节点的属性(注意区别“节点”与“对象”),默认为 true 。
  • attrName 实际的属性名,与“标准化”之后的属性名有区别。
这里
var app = angular.module('Demo', [], angular.noop);

app.directive('test', function(){
  var func = function($element, $attrs){
    $attrs.$set('b', 'ooo');
    $attrs.$set('a-b', '11');
    $attrs.$set('c-d', '11', true, 'c_d');
    console.log($attrs);
  }

  return {compile: func,
          restrict: 'E'}
});

app.controller('TestCtrl', function($scope){
  $scope.show = function(v){console.log(v);}
});

从例子中可以看到,原始的节点属性值对,放到对象中之后,名字一定是“标准化”之后的。但是手动 $set 的新属性,不会自动做标准化处理。

预定义的 NgModelController

在前面讲 conroller 参数的时候,提到过可以为指令定义一个 conroller 。官方的实现中,有很多已定义的指令,这些指令当中,有两个已定义的 conroller ,它们是 NgModelController 和 FormController ,对应 ng-model 和 form 这两个指令(可以参照前面的“表单控件”一章)。

在使用中,除了可以通过 $scope 来取得它们的引用之外,也可以在自定义指令中通过 require 参数直接引用,这样就可以在 link 函数中使用 controller 去实现一些功能。

先看 NgModelController 。这东西的作用有两个,一是控制 ViewValue 与 ModelValue 之间的转换关系(你可以实现看到的是一个值,但是存到变量里变成了另外一个值),二是与 FormController 配合做数据校验的相关逻辑。

先看两个应该是最有用的属性:

  • $formatters 是一个由函数组成的列表,串行执行,作用是把变量值变成显示的值。
  • $parsers 与上面的方向相反,把显示的值变成变量值。

假设我们在变量中要保存一个列表的类型,但是显示的东西只能是字符串,所以这两者之间需要一个转换:

var app = angular.module('Demo', [], angular.noop);

app.directive('test', function(){
  var link = function($scope, $element, $attrs, $ctrl){

    $ctrl.$formatters.push(function(value){
      return value.join(',');
    });

    $ctrl.$parsers.push(function(value){
      return value.split(',');
    });
  }

  return {compile: function(){return link},
          require: 'ngModel',
          restrict: 'A'}
});

app.controller('TestCtrl', function($scope){
  $scope.a = [];
  //$scope.a = [1,2,3];
  $scope.show = function(v){
    console.log(v);
  }
});

上面在定义 test 这个指令, require 参数指定了 ngModel 。同时因为 DOM 结构, ng-model 是存在的。于是, link 函数中就可以获取到一个 NgModelController 的实例,即代码中的 $ctrl 。

我们添加了需要的过滤函数:

  • 从变量( ModelValue )到显示值( ViewValue )的过程, $formatters 属性,把一个列表变成一个字符串。
  • 从显示值到变量的过程, $parsers 属性,把一个字符串变成一个列表。

对于显示值和变量,还有其它的 API ,这里就不细说了。

另一部分,是关于数据校验的,放到下一章同 FormController 一起讨论。

预定义的 FormController

前面的“表单控制”那章,实际上讲的就是 FormController ,只是那里是从 scope 中获取到的引用。现在从指令定义的角度,来更清楚地了解 FormController 及 NgModelController 是如何配合工作的。

先说一下, form 和 ngForm 是官方定义的两个指令,但是它们其实是同一个东西。前者只允许以标签形式使用,而后者允许 EAC 的形式。DOM 结构中, form 标签不能嵌套,但是 ng 的指令没有这个限制。不管是 form 还是 ngForm ,它们的 controller 都被命名成了 form 。 所以 require 这个参数不要写错了。

FormController 的几个成员是很好理解的:

  • $pristine 表单是否被动过
  • $dirty 表单是否没被动过
  • $valid 表单是否检验通过
  • $invalid 表单是否检验未通过
  • $error 表单中的错误
  • $setDirty() 直接设置 $dirty 及 $pristine
var app = angular.module('Demo', [], angular.noop);

app.directive('test', function(){
  var link = function($scope, $element, $attrs, $ctrl){
    $scope.do = function(){
      //$ctrl.$setDirty();
      console.log($ctrl.$pristine); //form是否没被动过
      console.log($ctrl.$dirty); //form是否被动过
      console.log($ctrl.$valid); //form是否被检验通过
      console.log($ctrl.$invalid); //form是否有错误
      console.log($ctrl.$error); //form中有错误的字段
    }
  }

  return {compile: function(){return link},
          require: 'form',
          restrict: 'A'}
});

app.controller('TestCtrl', function($scope){
});

$error 这个属性,是一个对象, key 是错误名, value 部分是一个列表,其成员是对应的 NgModelController 的实例。

FormController 可以自由增减它包含的那些,类似于 NgModelController 的实例。在 DOM 结构上,有 ng-model 的 input 节点的 NgMoelController 会被自动添加。

  • $addControl() 添加一个 conroller
  • $removeControl() 删除一个 controller

这两个手动使用机会应该不会很多。被添加的实例也可以手动实现所有的 NgModelController 的方法

var app = angular.module('Demo', [], angular.noop);

app.directive('test', function(){
  var link = function($scope, $element, $attrs, $ctrl){
    $scope.add = function(){
      $ctrl.$addControl($scope.bb);
      console.log($ctrl);
    }
  }

  return {compile: function(){return link},
          require: 'form',
          restrict: 'A'}
});

app.directive('bb', function(){
  var controller = function($scope, $element, $attrs, $transclude){
    $scope.bb = this;
    this.$name = 'bb';
  }

  return {compile: angular.noop,
          restrict: 'E',
          controller: controller}
});

app.controller('TestCtrl', function($scope){
});

整合 FormController 和 NgModelController 就很容易扩展各种类型的字段:

var app = angular.module('Demo', [], angular.noop);

app.directive('input', function(){
  var link = function($scope, $element, $attrs, $ctrl){
    console.log($attrs.type);
    var validator = function(v){
      if(v == '123'){
        $ctrl.$setValidity('my', true);
        return v;
      } else {
        $ctrl.$setValidity('my', false);
        return undefined;
      }
    }

    $ctrl.$formatters.push(validator);
    $ctrl.$parsers.push(validator);
  }

  return {compile: function(){return link},
          require: 'ngModel',
          restrict: 'E'}
});

app.controller('TestCtrl', function($scope){
    $scope.show = function(){
      console.log($scope.f);
    }
});

虽然官方原来定义了几种 type ,但这不妨碍我们继续扩展新的类型。如果新的 type 参数值不在官方的定义列表里,那会按 text 类型先做处理,这其实什么影响都没有。剩下的,就是写我们自己的验证逻辑就行了。

上面的代码是参见官方的做法,使用格式化的过程,同时在里面做有效性检查。

示例:文本框

这个例子与官网上的那个例子相似。最终是要显示一个文本框,这个文本框由标题和内容两部分组成。而且标题和内容则是引用 controller 中的变量值。

HTML 部分的代码:

标题:

内容:

从这个期望实现效果的 HTML 代码中,我们可以考虑设计指令的实现方式:

  • 这个指令的使用方式是“标签”, 即 restrict 这个参数应该设置为 E 。
  • 节点的属性值是对 controller 变量的引用,那么我们应该在指令的 scope 中使用 = 的方式来指定成员值。
  • 最终的效果显示需要进行 DOM 结构的重构,那直接使用 template 就好了。
  • 自定义的标签在最终效果中是多余的,所有 replace 应该设置为 true 。

JS 部分的代码:

var app = angular.module('Demo', [], angular.noop);

app.directive('ysBlock', function(){
  return {compile: angular.noop,
          template: '
' + '

' + '{{ title }}' + '

{{ text }}
', replace: true, scope: {title: '=title', text: '=text'}, restrict: 'E'}; }); app.controller('TestCtrl', function($scope){ $scope.title = '标题在这里'; $scope.text = '内容在这里'; }); angular.bootstrap(document, ['Demo']);

可以看到,这种简单的组件式指令,只需要作 DOM 结构的变换即可实现,连 compile 函数都不需要写。

示例:模板控制语句 for

这个示例尝试实现一个重复语句,功能同官方的 ngRepeat ,但是使用方式类似于我们通常编程语言中的 for 语句:

  • {{ o }}, {{ name }}

同样,我们从上面的使用方式去考虑这个指令的实现:

  • 这是一个完全的控制指令,所以单个节点应该只有它一个指令起作用就好了,于是权重要比较高,并且“到此为止”—— priority 设置为 1000 , terminal 设置为 true 。
  • 使用时的语法问题。事实上浏览器会把 for 节点补充成一个正确的 HTML 结构,即里面的属性都会变成类似 o=”” 这样。我们通过节点的 outerHTML 属性取到字符串并解析取得需要的信息。
  • 我们把 for 节点之间的内容作为一个模板,并且通过循环多次渲染该模板之后把结果填充到合适的位置。
  • 在处理上面的那个模板时,需要不断地创建新 scope 的,并且 o 这个成员需要单独赋值。

注意:这里只是简单实现功能。官方的那个 ngRepeat 比较复杂,是做了专门的算法优化的。当然,这里的实现也可以是简单把 DOM 结构变成使用 ngRepeat 的形式 🙂

JS 部分代码:

var app = angular.module('Demo', [], angular.noop);

app.directive('for', function($compile){
  var compile = function($element, $attrs, $link){
    var match = $element[0].outerHTML.match('');
    if(!match || match.length != 3){throw Error('syntax: ')}
    var iter = match[1];
    var list = match[2];
    var tpl = $compile($.trim($element.html()));
    $element.empty();

    var link = function($scope, $ielement, $iattrs, $controller){

      var new_node = [];

      $scope.$watch(list, function(list){
        angular.forEach(new_node, function(n){n.remove()});
        var scp, inode;
        for(var i = 0, ii = list.length; i < ii; i++){
          scp = $scope.$new();
          scp[iter] = list[i];
          inode = tpl(scp, angular.noop);
          $ielement.before(inode);
          new_node.push(inode);
        }

      });
    }

    return link;
  }
  return {compile: compile,
          priority: 1000,
          terminal: true,
          restrict: 'E'};
});

app.controller('TestCtrl', angular.noop);
angular.bootstrap(document, ['Demo']);

示例:模板控制语句 if/else

这个示例是尝试实现:

判断为真, {{ name }}

判断为假, {{ name }}

a:

name:

考虑实现的思路:

  • else 与 if 是两个指令,它们是父子关系。通过 scope 可以联系起来。至于 scope 是在 link 中处理还是 controller 中处理并不重要。
  • true 属性的条件判断通过 $parse 服务很容易实现。
  • 如果最终效果要去掉 if 节点,我们可以使用注释节点来“占位”。

JS 代码:

var app = angular.module('Demo', [], angular.noop);

app.directive('if', function($parse, $compile){
  var compile = function($element, $attrs){
    var cond = $parse($attrs.true);
    
    var link = function($scope, $ielement, $iattrs, $controller){
      $scope.if_node = $compile($.trim($ielement.html()))($scope, angular.noop);
      $ielement.empty();
      var mark = $('');
      $element.before(mark);
      $element.remove();

      $scope.$watch(function(scope){
        if(cond(scope)){
          mark.after($scope.if_node);
          $scope.else_node.detach();
        } else {
          if($scope.else_node !== undefined){
            mark.after($scope.else_node);
            $scope.if_node.detach();
          }
        }
      });
    }
    return link;
  }

  return {compile: compile,
          scope: true,
          restrict: 'E'}
});

app.directive('else', function($compile){
  var compile = function($element, $attrs){
    
    var link = function($scope, $ielement, $iattrs, $controller){
      $scope.else_node = $compile($.trim($ielement.html()))($scope, angular.noop);
      $element.remove();
    }
    return link;
  }

  return {compile: compile,
          restrict: 'E'}
});

app.controller('TestCtrl', function($scope){
  $scope.a = 1;
});

angular.bootstrap(document, ['Demo']);

代码中注意一点,就是 if_node 在得到之时,就已经是做了变量绑定的了。错误的思路是,在 $watch 中再去不断地得到新的 if_node 。

上一篇: 最后文章