Angular学习笔记(5)—指令详解

指令定义

对于指令,可以把它简单的理解成在特定DOM元素上运行的函数,指令可以扩展这个元素的功能。
  我们可以自己创造新的指令。directive()这个方法是用来定义指令的:

angular.module('myApp', [])
    .directive('myDirective', function ($timeout, UserDefinedService) {
        // 指令定义放在这里
    });

directive()方法可以接受两个参数:

  • name(字符串)
    指令的名字,用来在视图中引用特定的指令。要用驼峰式写法。
  • factory_function (函数)
    这个函数返回一个对象,其中定义了指令的全部行为。$compile服务利用这个方法返回的对象,在DOM调用指令时来构造指令的行为。
angular.application('myApp', [])
    .directive('myDirective', function() {
        return {
            // 通过设置项来定义指令,在这里进行覆写
        };
    });

我们也可以返回一个函数代替对象来定义指令,但是像上面的例子一样,通过对象来定义是最佳的方式。当返回一个函数时,这个函数通常被称作链接传递函数,利用它我们可以定义指令的链接功能。由于返回函数而不是对象会限制定义指令时的自由度,因此只在构造简单的指令时才比较有用。
  当AngularJS启动应用时,它会把第一个参数当作一个字符串,并以此字符串为名来注册第二个参数返回的对象。AngularJS编译器会解析主HTML的DOM中的元素、属性、注释和CSS类名中使用了这个名字的地方,并在这些地方引用对应的指令。当它找到某个已知的指令时,就会在页面中插入指令所对应的DOM元素。

<div my-directive></div>

指令的工厂函数只会在编译器第一次匹配到这个指令时调用一次。和controller函数类似,我们通过$injetor.invoke来调用指令的工厂函数。当AngularJS在DOM中遇到具名的指令时,会去匹配已经注册过的指令,并通过名字在注册过的对象中查找。此时,就开始了一个指令的生命周期,指令的生命周期开始于$compile方法并结束于link方法。
  定义一个指令时可以使用的全部设置选项:

angular.module('myApp', [])
       .directive('myDirective', function() {
           return {
               restrict: String,
               priority: Number,
               terminal: Boolean,
               template: String or Template Function:
               function(tElement, tAttrs) (...},
               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: // 返回一个对象或连接函数,如下所示:
               function(tElement, tAttrs, transclude) {
                   return {
                       pre: function(scope, iElement, iAttrs, controller) { ... },
                       post: function(scope, iElement, iAttrs, controller) { ... }
                   }
                   // 或者
                   return function postLink(...) { ... }
               }
           };
       });

restrict(字符串)

restrict是一个可选参数。它告诉AngularJS这个指令在DOM中可以何种形式被声明。默认restrict的值是A,即以属性的形式来进行声明。
可选值如下:

  • E(元素)
<my-directive></my-directive>
  • A(属性,默认值)
<div my-directive="expression"></div>
  • C(类名)
<div class="my-directive:expression;"></div>
  • M(注释)
<--directive:my-directive expression-->

这些选项可以单独使用,也可以混合在一起使用:

angular.module('myDirective', function(){
    return {
        restrict: 'EA' // 输入元素或属性
    };
});

上面的配置可以同时用属性或元素的方式来声明指令:

<-- 作为一个属性 -->
<div my-directive></div>
<-- 或者作为一个元素 -->
<my-directive></my-directive>

属性是用来声明指令最常用的方式,因为它能在包括老版本的IE浏览器在内的所有浏览器中正常工作,并且不需要在文档头部注册新的标签。

优先级priority(数值型)

优先级参数可以被设置为一个数值。大多数指令会忽略这个参数,使用默认值0,但也有些场景设置高优先级是非常重要的。
  如果一个元素上具有两个优先级相同的指令,声明在前面的那个会被优先调用。具有更高优先级的指令总是优先运行。
  ngRepeat是所有内置指令中优先级最高的,它总是在其他指令之前运行。

terminal(布尔型)

这个参数用来告诉AngularJS停止运行当前元素上比本指令优先级低的指令。但同当前指令优先级相同的指令还是会被执行。
  如果元素上某个指令设置了terminal参数并具有较高的优先级,就不要再用其他低优先级的指令对其进行修饰了,因为不会被调用。但是具有相同优先级的指令还是会被继续调用。
  使用了terminal参数的例子是ngViewngIfngIf的优先级略高于ngView,如果ngIf的表达式值为true,ngView就可以被正常执行,但如果ngIf表达式的值为false,由于ngView的优先级较低就不会被执行。

template(字符串或函数)

template参数是可选的,必须被设置为以下两种形式之一:

  • 一段HTML文本;
  • 一个可以接受两个参数的函数,参数为tElementtAttrs,并返回一个代表模板的字符串。tElementtAttrs中的t代表template,是相对于instance的。

AngularJS会同处理HTML一样处理模板字符串。模板中可以通过大括号标记来访问作用域,例如{{ expression }}。
如果模板字符串中含有多个DOM元素,或者只由一个单独的文本节点构成,那它必须被包含在一个父元素内。换句话说,必须存在一个根DOM元素:

template: '\
    <div> <-- single root element -->\
    <a href="http://google.com">Click me</a>\
    <h1>When using two elements, wrap them in a parent element</h1>\
</div>\

另外,注意每一行末尾的反斜线,这样AngularJS才能正确解析多行字符串。在实际生产中,更好的选择是使用templateUrl参数引用外部模板。

templateUrl(字符串或函数)

templateUrl是可选的参数,可以是以下类型:

  • 一个代表外部HTML文件路径的字符串;
  • 一个可以接受两个参数的函数,参数为tElementtAttrs,并返回一个外部HTML文件路径的字符串。

默认情况下,调用指令时会在后台通过Ajax来请求HTML模板文件。有两件事情需要知道。

  • 在本地开发时,需要在后台运行一个本地服务器,用以从文件系统加载HTML模板,否则会导致Cross Origin Request Script(CORS)错误。
  • 模板加载是异步的,意味着编译和链接要暂停,等待模板加载完成。

模板加载后,AngularJS会将它默认缓存到$templateCache服务中。在实际生产中,可以提前将模板缓存到一个定义模板的JS文件中,这样就不需要通过XHR来加载模板了。

replace(布尔型)

replace是一个可选参数,如果设置了这个参数,值必须为true,因为默认值为false。默认值意味着模板会被当作子元素插入到调用此指令的元素内部:

<div some-directive></div>
.directive('someDirective', function() {
    return {
        template: '<div>some stuff here<div>'
    };
});
<!--调用指令之后的结果如下(默认replace为false时的情况):-->
<div some-directive>
    <div>some stuff here<div>
</div>

如果replace被设置为了true:

.directive('someDirective', function() {
    return {
        replace: true 
        template: '<div>some stuff here<div>'
    };
});
<!--指令调用后的结果将是:-->
<div>some stuff here<div>

scope参数(布尔型或对象)

scope参数是可选的,可以被设置为true或一个对象。默认值是false。当scope设置为true时,会从父作用域继承并创建一个新的作用域对象。
  如果一个元素上有多个指令使用了隔离作用域,其中只有一个可以生效。只有指令模板中的根元素可以获得一个新的作用域。因此,对于这些对象来说scope默认被设置为true。
  内置指令ng-controller的作用,就是从父级作用域继承并创建一个新的子作用域。它会创建一个新的从父作用域继承而来的子作用域。
  为了进一步证明作用域的继承机制是向下而非向上进行的,下面的例子展示的是{{aThirdProperty}}从父作用域继承而来:

<!--HTML-->
<div ng-app="myApp" ng-init="someProperty = 'some data'"></div>
<div ng-init="siblingProperty='moredata'">
    Inside Div Two: {{aThirdProperty}}
    <div ng-init="aThirdProperty = 'data for 3rd property'" ng-controller="SomeController">
        Inside Div Three: {{aThirdProperty}}
        <div ng-controller="SecondController">
            Inside Div Four: {{aThirdProperty}}
        </div>
    </div>
</div>
<!--JS-->
angular.module('myApp', [])
    .controller('SomeController', function($scope) {
        // 可以留空,但需要被定义
    })
    .controller('SecondController', function($scope) {
        // 同样可以留空
    })

如果要创建一个能够从外部原型继承作用域的指令,将scope属性设置为true:

angular.module('myApp', [])
    .directive('myDirective', function() {
        return {
            restrict: 'A',
            scope: true
        };
    });

下面用指令来改变DOM的作用域:

<body ng-app="myApp">
<div ng-app="myApp" ng-init="someProperty = 'some data'">
<div ng-init="siblingProperty='moredata'">
    Inside Div Two: {{aThirdProperty}}
    <div ng-init="aThirdProperty = 'data for 3rd property'" ng-controller="SomeController">
        Inside Div Three: {{aThirdProperty}}
        <div ng-controller="SecondController">
            Inside Div Four: {{aThirdProperty}}
            <br>
            Outside myDirective: {{myProperty}}
            <div my-directive ng-init="myProperty = 'wow, this is cool'">
                Inside myDirective: {{myProperty}}
            </div>
        </div>
    </div>
</div>
</div>
</body>

隔离作用域

具有隔离作用域的指令最主要的使用场景是创建可复用的组件,组件可以在未知上下文中使用,并且可以避免污染所处的外部作用域或不经意地污染内部作用域。
  创建具有隔离作用域的指令需要将scope属性设置为一个空对象{}。如果这样做了,指令的模板就无法访问外部作用域了:

<div ng-controller='MainController'>
    Outside myDirective: {{myProperty}}
    <div my-directive ng-init="myProperty = 'wow, this is cool'">
        Inside myDirective: {{myProperty}}
    </div>
</div>
angular.module('myApp', [])
    .controller('MainController', function($scope) {
    })
    .directive('myDirective', function() {
        return {
            restrict: 'A',
            scope: {},
            priority: 100,
            template: '<div>Inside myDirective {{myProperty}}</div>'
        };
    });

注意,这里为myDirective设置了一个高优先级。由于ngInit指令会以非零的优先级运行,这个例子将会优先运行ngInit指令,然后才是我们定义的指定,并且这个myProperty$scope对象中是有效的。
  示例代码的效果与将scope设置为true几乎是相同的。下面看一下使用继承作用域的指令的例子,对比一下二者:

<div ng-init="myProperty='wow,thisiscool'">
    Surrounding scope: {{ myProperty }}
    <div my-inherit-scope-directive></div>
    <div my-directive></div>
</div>

angular.module('myApp', [])
    .directive('myDirective', function() {
        return {
            restrict: 'A',
            template: 'Inside myDirective, isolate scope: {{ myProperty }}',
            scope: {}
        };
    })
    .directive('myInheritScopeDirective', function() {
        return {
            restrict: 'A',
            template: 'Inside myDirective, isolate scope: {{ myProperty }}',
            scope: true
        };
    });

绑定策略

使用无数据的隔离作用域并不常见。AngularJS提供了几种方法能够将指令内部的隔离作用域,同指令外部的作用域进行数据绑定。
  为了让新的指令作用域可以访问当前本地作用域中的变量,需要使用下面三种别名中的一种。
  本地作用域属性:使用@符号将本地作用域同DOM属性的值进行绑定。指令内部作用域可以使用外部作用域的变量:

  • @ (or @attr)
    现在,可以在指令中使用绑定的字符串了。
    双向绑定:通过=可以将本地作用域上的属性同父级作用域上的属性进行双向的数据绑定。就像普通的数据绑定一样,本地属性会反映出父数据模型中所发生的改变。
  • = (or =attr)
    父级作用域绑定 通过&符号可以对父级作用域进行绑定,以便在其中运行函数。意味着对这个值进行设置时会生成一个指向父级作用域的包装函数。
    要使调用带有一个参数的父方法,我们需要传递一个对象,这个对象的键是参数的名称,值是要传递给参数的内容。
  • & (or &attr)

例如,假设我们在开发一个电子邮件客户端,并且要创建一个电子邮件的文本输入框:

<input type="text" ng-model="to"/>
<!-- 调用指令 -->
<div scope-example ng-model="to" on-send="sendMail(email)" from-name="ari@fullstack.io" ></div>

在指令中做如下设置以访问这些内容:

scope: {
    ngModel: '=', // 将ngModel同指定对象绑定
    onSend: '&', // 将引用传递给这个方法
    fromName: '@' // 储存与fromName相关联的字符串
}

transclude

transclude是一个可选的参数。如果设置了,其值必须为true,它的默认值是false。
  嵌入通常用来创建可复用的组件,典型的例子是模态对话框或导航栏。
  我们可以将整个模板,包括其中的指令通过嵌入全部传入一个指令中。这样做可以将任意内容和作用域传递给指令。transclude参数就是用来实现这个目的的,指令的内部可以访问外部指令的作用域,并且模板也可以访问外部的作用域对象。
  为了将作用域传递进去,scope参数的值必须通过{}或true设置成隔离作用域。如果没有设置scope参数,那么指令内部的作用域将被设置为传入模板的作用域。
  只有当你希望创建一个可以包含任意内容的指令时,才使用transclude: true
  嵌入允许指令的使用者方便地提供自己的HTML模板,其中可以包含独特的状态和行为,并对指令的各方面进行自定义。
  下面一起来实现个小例子,创建一个可以被自定义的可复用指令。
  例如,假设我们想创建一个包括标题和少量HTML内容的侧边栏,如下所示:

<div sideboxtitle="Links">
    <ul>
        <li>First link</li>
        <li>Second link</li>
    </ul>
</div>
angular.module('myApp', [])
    .directive('sidebox', function() {
        return {
            restrict: 'EA',
            scope: {
                title: '@'
            },
            transclude: true,
            template: '<div class="sidebox">\
                <div class="content">\
                <h2 class="header">{{ title }}</h2>\
                <span class="content" ng-transclude></span>\
                </div>\
                </div>'
        };
    });

这段代码告诉AngularJS编译器,将它从DOM元素中获取的内容放到它发现ng-transclude指令的地方。
  借助transclusion,我们可以将指令复用到第二个元素上,而无须担心样式和布局的一致性问题。
  例如,下面的代码会产生两个样式完全一致的侧边栏。

<div sideboxtitle="Links">
    <ul>
        <li>First link</li>
        <li>Second link</li>
    </ul>
</div>
<div sideboxtitle="TagCloud">
    <div class="tagcloud">
        <a href="">Graphics</a>
        <a href="">AngularJS</a>
        <a href="">D3</a>
        <a href="">Front-end</a>
        <a href="">Startup</a>
    /div>
</div>

如果指令使用了transclude参数,那么在控制器中就无法正常监听数据模型的变化了。这就是最佳实践总是建议在链接函数里使用$watch服务的原因。

controller(字符串或函数)

controller参数可以是一个字符串或一个函数。当设置为字符串时,会以字符串的值为名字,来查找注册在应用中的控制器的构造函数:

angular.module('myApp', [])
    .directive('myDirective', function() {
        restrict: 'A', // 始终需要
        controller: 'SomeController'
    })
// 应用中其他的地方,可以是同一个文件或被index.html包含的另一个文件
angular.module('myApp')
    .controller('SomeController', function($scope, $element, $attrs, $transclude) {
        // 控制器逻辑放在这里
    });

可以在指令内部通过匿名构造函数的方式来定义一个内联的控制器:

angular.module('myApp',[])
    .directive('myDirective', function() {
        restrict: 'A',
        controller:
        function($scope, $element, $attrs, $transclude) {
            // 控制器逻辑放在这里
        }
    });

我们可以将任意可以被注入的AngularJS服务传递给控制器。例如,如果我们想要将$log服务传入控制器,只需简单地将它注入到控制器中,便可以在指令中使用它了。
控制器中也有一些特殊的服务可以被注入到指令当中。这些服务有:

  1. $scope
    与指令元素相关联的当前作用域。
  2. $element
    当前指令对应的元素。
  3. $attrs
    由当前元素的属性组成的对象。例如,下面的元素:
    <div id="aDiv"class="box"></div>
    具有如下的属性对象:
{
    id: "aDiv",
    class: "box"
}
  1. $transclude
    嵌入链接函数会与对应的嵌入作用域进行预绑定。
    transclude链接函数是实际被执行用来克隆元素和操作DOM的函数。

在控制器内部操作DOM是和AngularJS风格相悖的做法,但通过链接函数就可以实现这个需求。仅在compile参数中使用transcludeFn是推荐的做法。
  例如,我们想要通过指令来添加一个超链接标签。可以在控制器内的$transclude函数中实现,如下所示:

angular.module('myApp')
    .directive('link', function() {
        return {
            restrict: 'EA',
            transclude: true,
            controller:
            function($scope, $element, $transclude, $log) {
                $transclude(function(clone) {
                    var a = angular.element('<a>');
                    a.attr('href', clone.text());
                    a.text(clone.text());
                    $log.info("Created new a tag in link directive");
                    $element.append(a);
                });
            }
        };
    });

指令的控制器和link函数可以进行互换。控制器主要是用来提供可在指令间复用的行为,但链接函数只能在当前内部指令中定义行为,且无法在指令间复用。
  link函数可以将指令互相隔离开来,而controller则定义可复用的行为。
  由于指令可以require其他指令所使用的控制器,因此控制器常被用来放置在多个指令间共享的动作。
  如果我们希望将当前指令的API暴露给其他指令使用,可以使用controller参数,否则可以使用link来构造当前指令元素的功能性。如果我们使用了scope.$watch()或者想要与DOM元素做实时的交互,使用链接会是更好的选择。
  技术上讲,$scope会在DOM元素被实际渲染之前传入到控制器中。在某些情况下,例如使用了嵌入,控制器中的作用域所反映的作用域可能与我们所期望的不一样,这种情况下,$scope对象无法保证可以被正常更新。
  当想要同当前屏幕上的作用域交互时,可以使用被传入到link函数中的scope参数。

controllerAs(字符串)

controllerAs参数用来设置控制器的别名,可以以此为名来发布控制器,并且作用域可以访问controllerAs。这样就可以在视图中引用控制器,甚至无需注入$scope
  例如,创建一个MainController,然后不要注入$scope,如下所示:

angular.module('myApp')
.controller('MainController', function() {
this.name = "Ari";
});

现在,在HTML中无需引用作用域就可以使用MainController

<div ng-app ng-controller="MainController">
<input type="text" ng-model="main.name" />
<span>{{ main.name }}</span>
</div>

这个参数看起来好像没什么大用,但它给了我们可以在路由和指令中创建匿名控制器的强大能力。这种能力可以将动态的对象创建成为控制器,并且这个对象是隔离的、易于测试的。
例如,可以在指令中创建匿名控制器,如下所示:

angular.module('myApp')
.directive('myDirective', function() {
return {
restrict: 'A',
template: '<h4>{{ myController.msg }}</h4>',
controllerAs: 'myController',
controller: function() {
this.msg = "Hello World"
}
};
});

require(字符串或数组)

require参数可以被设置为字符串或数组,字符串代表另外一个指令的名字。require会将控制器注入到其值所指定的指令中,并作为当前指令的链接函数的第四个参数。
  字符串或数组元素的值是会在当前指令的作用域中使用的指令名称。
  scope会影响指令作用域的指向,是一个隔离作用域,一个有依赖的作用域或者完全没有作用域。在任何情况下,AngularJS编译器在查找子控制器时都会参考当前指令的模板。
  如果不使用^前缀,指令只会在自身的元素上查找控制器。

//...
restrict: 'EA',
require: 'ngModel'
//...

指令定义只会查找定义在指令作当前用域中的ng-model=""

<!-- 指令会在本地作用域查找ng-model -->
<div my-directive ng-model="object"></div>

require参数的值可以用下面的前缀进行修饰,这会改变查找控制器时的行为:
?
如果在当前指令中没有找到所需要的控制器,会将null作为传给link函数的第四个参数。
^
如果添加了^前缀,指令会在上游的指令链中查找require参数所指定的控制器。
?^
将前面两个选项的行为组合起来,我们可选择地加载需要的指令并在父指令链中进行查找。
没有前缀
如果没有前缀,指令将会在自身所提供的控制器中进行查找,如果没有找到任何控制器(或具有指定名字的指令)就抛出一个错误。

AngularJS 的生命周期

在AngularJS应用起动前,它们以HTML文本的形式保存在文本编辑器中。应用启动后会进行编译和链接,作用域会同HTML进行绑定,应用可以对用户在HTML中进行的操作进行实时响应。
在这个过程中总共有两个主要阶段。

编译阶段

第一个阶段是编译阶段。在编译阶段,AngularJS会遍历整个HTML文档并根据JavaScript中的指令定义来处理页面上声明的指令。
  每一个指令的模板中都可能含有另外一个指令,另外一个指令也可能会有自己的模板。当AngularJS调用HTML文档根部的指令时,会遍历其中所有的模板,模板中也可能包含带有模板的指令。
  模板树可能又大又深,但有一点需要注意,尽管元素可以被多个指令所支持或修饰,这些指令本身的模板中也可以包含其他指令,但只有属于最高优先级指令的模板会被解析并添加到模板树中。这里有一个建议,就是将包含模板的指令和添加行为的指令分离开来。如果一个元素已经有一个含有模板的指令了,永远不要对其用另一个指令进行修饰。只有具有最高优先级的指令中的模板会被编译。
  一旦对指令和其中的子模板进行遍历或编译,编译后的模板会返回一个叫做模板函数的函数。我们有机会在指令的模板函数被返回前,对编译后的DOM树进行修改。
  在这个时间点DOM树还没有进行数据绑定,意味着如果此时对DOM树进行操作只会有很少的性能开销。基于此点,ng-repeatng-transclude等内置指令会在这个时候,也就是还未与任何作用域数据进行绑定时对DOM进行操作。
ng-repeat为例,它会遍历指定的数组或对象,在数据绑定之前构建出对应的DOM结构。
  如果我们用ng-repeat来创建无序列表,其中的每一个<li>都会被ng-click指令所修饰,这个过程会使得性能比手动创建列表要快得多,尤其是列表中含有上百个元素时。与克隆<li>元素,再将其与数据进行链接,然后对每个元素都循环进行此操作的过程不同,我们仅需要先将无需列表构造出来,然后将新的DOM(编译后的DOM)传递给指令生命周期中的下一个阶段,即链接阶段。
  一个指令的表现一旦编译完成,马上就可以通过编译函数对其进行访问,编译函数的签名包含有访问指令声明所在的元素(tElemente)及该元素其他属性(tAttrs)的方法。这个编译函数返回前面提到的模板函数,其中含有完整的解析树。
  这里的重点是,由于每个指令都可以有自己的模板和编译函数,每个模板返回的也都是自己的模板函数。链条顶部的指令会将内部子指令的模板合并在一起成为一个模板函数并返回,但在树的内部,只能通过模板函数访问其所处的分支。
  最后,模板函数被传递给编译后的DOM树中每个指令定义规则中指定的链接函数。

compile(对象或函数)

compile选项可以返回一个对象或函数。
  compile选项本身并不会被频繁使用,但是link函数则会被经常使用。本质上,当我们设置了link选项,实际上是创建了一个postLink()链接函数,以便compile()函数可以定义链接函数。
  通常情况下,如果设置了compile函数,说明我们希望在指令和实时数据被放到DOM中之前进行DOM操作,在这个函数中进行诸如添加和删除节点等DOM操作是安全的。
  compilelink选项是互斥的。如果同时设置了这两个选项,那么会把compile所返回的函数当作链接函数,而link选项本身则会被忽略。

// ...
compile: function(tEle, tAttrs, transcludeFn) {
    var tplEl = angular.element('<div><h2></h2></div>');
    var h2 = tplEl.find('h2');
    h2.attr('type', tAttrs.type);
    h2.attr('ng-model', tAttrs.ngModel);
    h2.val("hello");
    tEle.replaceWith(tplEl);
    return function(scope, ele, attrs) {
        // 连接函数
    };
}
//...

如果模板被克隆过,那么模板实例和链接实例可能是不同的对象。因此在编译函数内部,我们只能转换那些可以被安全操作的克隆DOM节点。不要进行DOM事件监听器的注册:这个操作应该在链接函数中完成。
  编译函数负责对模板DOM进行转换。
  链接函数负责将作用域和DOM进行链接。在作用域同DOM链接之前可以手动操作DOM。在实践中,编写自定义指令时这种操作是非常罕见的,但有几个内置指令提供了这样的功能。

链接

link函数创建可以操作DOM的指令。
  链接函数是可选的。如果定义了编译函数,它会返回链接函数,因此当两个函数都定义了时,编译函数会重载链接函数。如果我们的指令很简单,并且不需要额外的设置,可以从工厂函数(回调函数)返回一个函数来代替对象。如果这样做了,这个函数就是链接函数。
  下面两种定义指令的方式在功能上是完全一样的:

angular.module('myApp', [])
    .directive('myDirective', function() {
        return {
            pre: function(tElement, tAttrs, transclude) {
                // 在子元素被链接之前执行
                // 在这里进行Don转换不安全
                // 之后调用'lihk'函数将无法定位要链接的元素
            },
            post: function(scope, iElement, iAttrs, controller) {
                // 在子元素被链接之后执行
                // 如果在这里省略掉编译选项
                //在这里执行DOM转换和链接函数一样安全吗
            }
        };
    });
angular.module('myApp', [])
    .directive('myDirective', function() {
        return {
            link: function(scope, ele, attrs) {
                return {
                    pre: function(tElement, tAttrs, transclude) {
                        // 在子元素被链接之前执行
                        // 在这里进行Don转换不安全
                        // 之后调用'lihk'h函数将无法定位要链接的元素
                    },
                    post: function(scope, iElement, iAttrs, controller) {
                        // 在子元素被链接之后执行
                        // 如果在这里省略掉编译选项
                        //在这里执行DOM转换和链接函数一样安全吗
                    }
                }
            }
        }
    });

当定义了编译函数来取代链接函数时,链接函数是我们能提供给返回对象的第二个方法,也就是postLink函数。本质上讲,这个事实正说明了链接函数的作用。它会在模板编译并同作用域进行链接后被调用,因此它负责设置事件监听器,监视数据变化和实时的操作DOM。
  link函数对绑定了实时数据的DOM具有控制能力,因此需要考虑性能问题。
链接函数的签名如下:

link: function(scope, element, attrs) {
// 在这里操作DOM
}

如果指令定义中有require选项,函数签名中会有第四个参数,代表控制器或者所依赖的指令的控制器。

// require 'SomeController',
link: function(scope, element, attrs, SomeController) {
// 在这里操作DOM,可以访问required指定的控制器
}

如果require选项提供了一个指令数组,第四个参数会是一个由每个指令所对应的控制器组成的数组。
下面看一下链接函数中的参数:

  • scope
    指令用来在其内部注册监听器的作用域。
  • iElement
    iElement参数代表实例元素,指使用此指令的元素。在postLink函数中我们应该只操作此元素的子元素,因为子元素已经被链接过了。
  • iAttrs
    iAttrs参数代表实例属性,是一个由定义在元素上的属性组成的标准化列表,可以在所有指令的链接函数间共享。会以JavaScript对象的形式进行传递。
  • controller
    controller参数指向require选项定义的控制器。如果没有设置require选项,那么controller参数的值为undefined
    控制器在所有的指令间共享,因此指令可以将控制器当作通信通道(公共API)。如果设置了多个require,那么这个参数会是一个由控制器实例组成的数组,而不只是一个单独的控制器。

ngModel

ngModel是一个用法特殊的指令,它提供更底层的API来处理控制器内的数据。当我们在指令中使用ngModel时能够访问一个特殊的API,这个API用来处理数据绑定、验证、CSS更新等不实际操作DOM的事情。
  ngModel控制器会随ngModel被一直注入到指令中,其中包含了一些方法。为了访问ngModelController必须使用require设置:

angular.module('myApp')
    .directive('myDirective',function(){
        return {
            require: '?ngModel',
            link: function(scope, ele, attrs, ngModel) {
                if (!ngModel) return;
                // 现在我们的指令中已经有ngModelController的一个实例
            }
        };
    });

如果不设置require选项,ngModelController就不会被注入到指令中。
  注意,这个指令没有隔离作用域。如果给这个指令设置隔离作用域,将导致内部ngModel无法更新外部ngModel的对应值:AngularJS会在本地作用域以外查询值。
  为了设置作用域中的视图值,需要调用ngModel.$setViewValue()函数。ngModel.$setViewValue()函数可以接受一个参数。
value(字符串):value参数是我们想要赋值给ngModel实例的实际值。这个方法会更新控制器上本地的$viewValue,然后将值传递给每一个$parser函数。
  当值被解析,且$parser流水线中所有的函数都调用完成后,值会被赋给$modelValue属性,并且传递给指令中ng-model属性提供的表达式。
  最后,所有步骤都完成后,$viewChangeListeners中所有的监听器都会被调用。
  注意,单独调用$setViewValue()不会唤起一个新的digest循环,因此如果想更新指令,需要在设置$viewValue后手动触发digest
  $setViewValue()方法适合于在自定义指令中监听自定义事件,我们会希望在回调时设置$viewValue并执行digest循环。

angular.module('myApp')
    .directive('myDirective', function() {
        return {
            require: '?ngModel',
            link: function(scope, ele, attrs, ngModel) {
                if (!ngModel) return;
                $(function() {
                    ele.datepicker({
                        onSelect: function(date) {
                            // 设置视图和调用apply
                            scope.$apply(function() {
                                ngModel.$setViewValue(date);
                            });
                        }
                    });
                });
            }
        };
    });

自定义渲染

在控制器中定义$render方法可以定义视图具体的渲染方式。这个方法会在$parser流水线完成后被调用。
由于这个方法会破坏AngularJS的标准工作方式,因此一定要谨慎使用:

angular.module('myApp')
    .directive('myDirective', function() {
        return {
            require: '?ngModel',
            link: function(scope, ele, attrs, ngModel) {
                if (!ngModel) return;
                ngModel.$render = function() {
                    element.html(ngModel.$viewValue() || 'None');
                };
            }
        };
    });

属性

ngModelController中有几个属性可以用来检查甚至修改视图。

  1. $viewValue
    $viewValue属性保存着更新视图所需的实际字符串。
  2. $modelValue
    $modelValue由数据模型持有。$modelValue$viewValue可能是不同的,取决于$parser流水线是否对其进行了操作。
  3. $parsers
    $parsers的值是一个由函数组成的数组,其中的函数会以流水线的形式被逐一调用。ngModel从DOM中读取的值会被传入$parsers中的函数,并依次被其中的解析器处理。
  4. $formatters
    $formatters的值是一个由函数组成的数组,其中的函数会以流水线的形式在数据模型的值发生变化时被逐一调用。它和$parser流水线互不影响,用来对值进行格式化和转换,以便在绑定了这个值的控件中显示。
  5. $viewChangeListeners
    $viewChangeListeners的值是一个由函数组成的数组,其中的函数会以流水线的形式在视图中的值发生变化时被逐一调用。通过$viewChangeListeners,可以在无需使用$watch的情况下实现类似的行为。由于返回值会被忽略,因此这些函数不需要返回值。
  6. $error
    $error对象中保存着没有通过验证的验证器名称以及对应的错误信息。
  7. $pristine
    $pristine的值是布尔型的,可以告诉我们用户是否对控件进行了修改。
  8. $dirty
    $dirty的值和$pristine相反,可以告诉我们用户是否和控件进行过交互。
  9. $valid
    $valid值可以告诉我们当前的控件中是否有错误。当有错误时值为false,没有错误时值为true。
  10. $invalid
    $invalid值可以告诉我们当前控件中是否存在至少一个错误,它的值和$valid相反。

自定义验证

要验证username在数据库中是否合法,可以实现一个指令,用来在表单发生变化时发送Ajax请求:

angular.module('validationExample', [])
    .directive('ensureUnique',function($http) {
        return {
            require: 'ngModel',
            link: function(scope, ele, attrs, c) {
                scope.$watch(attrs.ngModel, function() {
                    $http({
                        method: 'POST',
                        url: '/api/check/' + attrs.ensureUnique,
                        data: {field: attrs.ensureUnique, valud:scope.ngModel}
                    }).success(function(data,status,headers,cfg) {
                        c.$setValidity('unique', data.isUnique);
                    }).error(function(data,status,headers,cfg) {
                        c.$setValidity('unique', false);
                    });
                });
            }
        };
    });

<input type="text"
    placeholder="Desired username"
    name="username"
    ng-model="signup.username"
    ng-minlength="3"
    ng-maxlength="20"
    ensure-unique="username" required />

在这个自定义验证中,每当ngModel中对应的字段发生变化就会向服务器发送请求,以检查用户名是否是唯一的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容