Angular directive 實例詳解

準備代碼,會在實例中用到html

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

angular指令定義大體以下express

app.directive('directiveName', function() {
  return {
    // config
  }
})

其中return返回的配置對象包含不少參數,以下一一說明。數組

1. restrict

值爲字符串,可選參數,指明指令在DOM中以什麼形式被聲明緩存

<!-- E (element) -->
<directiveName></directiveName>

<!-- A (attribute) -->
<div directiveName="expression"></div>

<!-- C (class) -->
<div class="directiveName"></div>

<!-- M(comment) -->
<!--directive:directiveName expression-->

默認值爲restrict: 'EA',表示能夠在DOM裏面能夠用元素形式和屬性形式被聲明。通常來講,若是你想建立一個本身模板的組件時,則使用元素形式,可是僅僅是爲已有元素添加功能的話,就使用屬性名。服務器

若是想要支持IE8,則最好使用屬性和類形式來定義,另外從angular 1.3.X開始,已經放棄支持IE8了。app

2. priority

數字,可選參數,致命指令的優先級,若在單個DOM元素上有多個指令,則優先級高的先執行。cors

固然,設置指令的優先級不太經常使用,可是比較特殊的例子是,內置指令ng-repeat的優先級爲1000,而ng-init的優先級爲 450。異步

3. terminal

布爾型,可選參數,能夠被設置爲true或者false,若設置爲true,則優先級低於此指令的其餘指令則無效,不會被調用優先級相同任然會執行。函數

4. template

字符串或者函數,可選參數。post

能夠是一段html文本

app.directive('hello', function() {
  return {
    template: '<div><h3>Hello, world!</h3></div>'
  }
})

使用以下

<hello></hello>

渲染結果爲

<hello>
  <div>
    <h3>Hello, world!</h3>
  </div>
</hello>

也能夠是一個函數,可接受兩個參數Element與Attrs

其中Element是指使用此指令的元素,而Attrs則是實例的屬性,它是由一個元素上全部屬性組成的集合,形如

{
  title: 'test',
  id: 'testDiv',
  class: 'demo',
  input: 'text',
  ...
}

下面讓咱們看看template是一個函數時的狀況

app.directive('hello', function() {
  return {
    template: function(element, attrs) {
      return '<div>'+ attrs.title +'</div>'
    }
  };
});

html代碼

<hello title="message"></hello>

渲染結果

<hello>
  <div>message</div>
</hello>

實例地址

5. replace

布爾型,默認值爲false,設置爲true的時候,表示能夠用模板內容替換自定義的元素標籤。

在template的例子中,咱們發現渲染結果中包含有自定義的元素<hello></hello>,很顯然,這並非咱們想要的渲染結果,所以將replace設置爲true以後,hello標籤將會消失

app.directive('hello', function() {
  return {
    replace: true,
    template: function(element, attrs) {
      return '<div>'+ attrs.title +'</div>'
    }
  };
});

渲染結果以下,hello標籤消失不見

<div>message</div>

6. templateUrl

字符串或者函數,可選參數

可使一個表明html文件路徑的字符串,也能夠是一個函數,大體意思與template同樣。

在本地開發時,須要運行一個服務器,否則使用templateUrl會報錯

Cross origin request script(cors)

因爲加載html模板是經過異步加載,若加載大量的模板會拖慢網站的速度,這裏有一個技巧,就是先緩存模板,你能夠先在你的index頁面加載好,將下列代碼做爲你頁面的一部分包含在內

<script type="text/ng-template" id="demo.html">
  <div><!--這裏是模板內容--></div>
</script>

這裏的id屬性就是被設置在templateUrl上用的

另一種方法緩存以下

app.run(function($templateCache) {
  $templateCache.put('template.html', '<div>template</div>');
})

7. scope

布爾值或者對象,可選參數,默認值爲false,表示繼承父級做用域。

若是值爲true,表示繼承父做用域,並建立本身的做用域(子做用域)

若是爲對象,{},則表示建立一個全新的隔離做用域。

首先咱們須要來了解一下scope的繼承機制。咱們使用ng-controller這個指令來舉例。咱們都知道ng-controller能夠從父做用域中繼承並建立一個新的子做用域。以下:

<div ng-app="app" ng-init="aaa='parent'">
    parentNode: {{aaa}}
    
    <div ng-controller="demoController">
        childrenNode: {{aaa}}
    </div>
</div>
angular.module('app', [])
.controller('demoController', function($scope) {
    $scope.aaa = "children";
})

實例地址

這時頁面的顯示結果爲

parentNode: parent
childrenNode: children

當時當咱們並無在demoController的做用域中給aaa賦值,也就是在上例中刪除這一句$scope.aaa = "children";,那麼執行結果就爲

parentNode: parent
childrenNode: parent

注意:若是一個元素上有多個指令都使用了隔離做用域,那麼只有其中一個能夠生效,只有指令模板中的根元素才能得到一個新的做用域,這時候,scope就被設置爲true了。

<div ng-app="app" ng-init="aaa='parent'">
    parentNode: {{aaa}}
    
    <div ng-controller="demoController01">
        childrenNode: {{aaa}}
        
        <div ng-controller="demoController02">
            lastNode: {{aaa}}
        </div>
    </div>
</div>
angular.module('app', [])
.controller('demoController01', function($scope) {
    $scope.aaa = "children";
})
.controller('demoController02', function($scope) {
    $scope.aaa = "last"
})

實例地址

接下來,咱們經過一個簡單明瞭的例子來講明scope取值的不一樣差異

<div ng-app="app" ng-controller="mainController">
    parent: {{ name }} 
    <br />
    <input type="text" ng-model="name" />
    <div my-directive></div>
</div>
angular.module('app', [])
.controller('mainController', function($scope) {
    $scope.name = "Jake";
})
.directive('myDirective', function() {
    return {
        restrict: 'EA',
        scope: false,
        replace: true,
        template: '' +
            '<div>' +
                'childNode: {{ name }} ' +
                '<br />' +
                '<input type="text" ng-model="name">' +
            '</div>'
    }
})

實例地址

點擊上面的實例地址,咱們能夠依次改變scope的值爲false, true, {},結果發現

  • false:兒子繼承父親的值,改變父親的值,兒子的值也隨着改變,反之亦然,這就是繼承且不隔離
  • true:兒子繼承父親的值,改變父親的值,兒子的值也隨着改變,可是改變兒子的值,父親的值並無改變,這就是繼承可是隔離
  • {}:沒有繼承父親的值,因此兒子的值爲空,改變任何一方的值都不會影響另外一方,這就是不繼承且隔離

當想要建立一個可重用的組件時,隔離做用域是一個很好的選擇,經過隔離做用域,咱們能夠確保指令是獨立的,而且能夠輕鬆的插入到任何HTML APP中,而且這種作法防止了父做用域被污染。

隔離做用域能夠經過綁定策略來訪問父做用域的屬性

咱們來看一個例子

<div ng-app="app" ng-controller="mainController">
    <input type="text" ng-model="color" placeholder="Enter a color"/>
    <hello-world></hello-world>
</div>
angular.module('app', [])
.controller('mainController', function($scope) {
})
.directive('helloWorld', function() {
    return {
        restrict: 'EA',
        scope: false,
        replace: true,
        template: '<p style="background-color:{{color}}">Hello world!</p>'
    }
})

運行上面的代碼,咱們在input中輸入顏色值,好比red,那麼hello world一行的背景就會變成紅色。動手試試!

實例地址

可是,當咱們將scope的值設置爲{}時,再次運行代碼就發現頁面並不能成功的完整顯示了.這是由於{}讓helloWorld指令產生了隔離做用域,沒辦法從父級做用域中繼承到color的值了。

因此在template中的{{color}}變成了依賴於本身的做用域,而不是依賴於父級做用域。所以咱們須要一些辦法來讓隔離做用域可以讀取父級做用域的屬性,這個方法就是綁定策略。

下面咱們來探索設置這種綁定策略的幾種方法

方法一 使用@來進行單向文本(字符串)綁定
<div ng-app="app" ng-controller="mainController">
    <input type="text" ng-model="color" placeholder="Enter a color"/>
    <hello-world color-attr="{{color}}"></hello-world>
</div>
angular.module('app', [])
.controller('mainController', function($scope) {
})
.directive('helloWorld', function() {
    return {
        restrict: 'EA',
        scope: {
            color: '@colorAttr'
        },
        replace: true,
        template: '<p style="background-color:{{color}}">Hello world!</p>'
    }
})

實例地址

這種辦法只能單向,經過在運行的指令DOM上設置color-attr屬性,而且採用{{}}綁定某個模型值。固然,咱們也能夠直接在這裏綁定字符串的顏色值,如color-attr="red"

所以當表達式的值發生變化時,屬性color-attr的值也會發生變化,經過單向綁定該值,就能夠改變隔離做用域中的屬性color.

若是綁定的隔離做用域屬性名與元素的屬性名相同,則能夠採用缺省寫法

// html
<hello-world color="{{color}}"></hello-world>

// js
app.directive('helloWorld', function() {
   return {
       scope: { color: '@' },
       ...
   }
})
方法2、使用'='進行雙向綁定
<div ng-app="app" ng-controller="mainController">
    <input type="text" ng-model="color" placeholder="Enter a color"/>
    <br />
    {{ color }}
    <!-- 這裏要注意寫法與@綁定的不一樣 -->
    <hello-world color="color"></hello-world>
</div>
angular.module('app', [])
.controller('mainController', function($scope) {
    $scope.color = 'red';
})
.directive('helloWorld', function() {
    return {
        restrict: 'EA',
        scope: {
            color: '='
        },
        replace: true,
        template: '<div><p style="background-color:{{color}}">' +
            'Hello world!' +
        '</p>' + 
        '<input type="text" ng-model="color"></div>'
    }
})

實例地址

此處也採用了相似的缺省寫法。

這裏須要注意的是,咱們要直接在指令元素設置屬性時,是直接將實際的做用域模型複製給該屬性

這樣一個雙向綁定被創建了,改變任何一個input值都會改變另一個值。

方法3、使用'&'調用父做用域中的函數
<div ng-app="app" ng-controller="mainController">
    <input type="text" ng-model="name" placeholder="Enter name"/>
    <br />
    {{ name }}
    <hello-world say="say()" name="{{name}}"></hello-world>
</div>
angular.module('app', [])
.controller('mainController', function($scope) {
    $scope.name = "yangbo";
    $scope.say = function() {
        alert('hello!');
    }
})
.directive('helloWorld', function() {
    return {
        restrict: 'EA',
        scope: {
            name: '@',
            say: '&'
        },
        replace: true,
        template: '<button type="button" ng-bind="name" ng-init="say()"></button>'
    }
})

實例地址

一樣採用了缺省寫法,運行以後,彈出窗口!

8. transclude

布爾值或者字符element,默認值爲false

這個配置選項可讓咱們提取包含在指令那個元素裏面的內容,再將它放置在指令模板的特定位置。當咱們開啓transclude以後,咱們就可使用ng-transclude來指明應該在什麼地方放置transclude的內容

<div ng-app="app" ng-controller="mainController">
    <div class="a">
        <p>china</p>
        <hello-world>
            {{name}}
        </hello-world>
    </div>
</div>
angular.module('app', [])
.controller('mainController', function($scope) {
    $scope.name = "yangbo5207";
})
.directive('helloWorld', function() {
    return {
        restrict: 'EA',
        scope: {},
        replace: true,
        transclude: true,
        template: '<div class="b"><div ng-transclude>你看不見我</div></div>'
    }
})

運行上面的代碼,輸出

china
yangbo5207

咱們查看渲染出來的html,結果爲

<div ng-app="app" ng-controller="mainController" class="ng-scope">
    <div class="a">
        <p>china</p>
        <div class="b ng-isolate-scope">
            <div ng-transclude="">
                <span class="ng-binding ng-scope">yangbo5207</span>
            </div>
        </div>
    </div>
</div>

另外當開啓transclude時,會建立一個新的transclude空間,而且繼承父做用域(也就是scope設置的隔離做用域)

從上面例子咱們知道,當transclude的值被設置爲true時,嵌入的內容爲{{name}},可是若是將它的值設置爲element呢,咱們能夠在上例的基礎上進行一個簡單的修改,發現,嵌入內容爲整個元素

<hello-world>{{name}}</hello-world>

查看渲染結果

<div ng-app="app" ng-controller="mainController" class="ng-scope">
    <div class="a">
        <p>china</p>
        <div class="b ng-isolate-scope">
          <div ng-transclude="">
            <hello-world class="ng-binding ng-scope">
              yangbo5207
            </hello-world>
          </div>
        </div>
    </div>
</div>

注意:在一個指令的模板中,只能聲明一次ng-transclude

那麼問題來了,若是咱們想要把嵌入內容屢次放入咱們的模板中怎麼辦?

可使用$transclude,後面會講到!也可使用complie函數中,裏面的transcludeFn參數,後面會講到!或者使用link鏈接函數

9. controller

能夠是一個字符串或者函數。

若爲字符串,則將字符串當作控制器的名字,來查找註冊在應用中的控制器的構造函數

angular.module('app', [])
.directive('myDirective', function() {
    return {
        restrict: 'EA',
        replace: true,
        transclude: true,
        
        // 會查找模塊中其餘被名爲targetController的控制器
        controller: 'targetController'
    }
})
.controller('targetController', function($scope, $element, $attrs, $transclude) {
    // 控制器邏輯放在這裏
})

固然,也能夠直接在指令內部定義爲匿名函數,一樣能夠注入任何服務

angular.module('app', [])
.directive('myDirective', function() {
    return {
        restrict: 'EA',
        replace: true,
        transclude: true,
        controller: function($scope, $element, $attrs, $transclude) {
            // 控制器邏輯
        }
    }
})

另外還有一些特殊的服務能夠注入,

  • $scope 與指令元素相關聯的做用域
  • $element 當前指令對應的元素
  • $attrs 當前元素的屬性組成的對象
  • $transclude 嵌入連接函數,實際被執行用來克隆元素和操做DOM中的函數(除非是用來定義一些可複用的行爲,不然通常不推薦在這使用)

指令的控制器和link函數(後面會講到)能夠進行互換。區別在於,控制器主要用來提供可在指令間複用的行爲,可對外提供與外部交互的接口,可是link連接只能在當前指令內部中定義行爲,且沒法在指令間複用。

<div ng-app="app">
    <div test-directive="dateType" url="http://www.tigerbrokers.com">外面的世界很精彩。</div>
</div>
angular.module('app', [])
.directive('testDirective', function() {
    return {
        transclude: true,
        replace: true,
        controller: function($scope, $element, $attrs, $transclude, $log) {
            $transclude(function(clone) {
                var a = angular.element('<a>');
                a.attr('href', $attrs.url);
                a.text(clone.text());
                $element.append(a);
            });
        }
    }
})

實例地址

$transclude 能夠接受兩個參數,第一個是$scope,第二個是纔有參數clone的回調函數。

而這個clone實際上就是嵌入的內容。能夠在根據它作不少DOM操做。

它還有最簡單的用法

angular.module('app', [])
.directive('testDirective', function() {
    return {
        transclude: true,
        replace: true,
        controller: function($scope, $element, $attrs, $transclude, $log) {
            
            // 這裏的$transclude就是嵌入的內容
            var a = $transclude();
            $element.append(a);
        }
    }
})

可是咱們要注意,使用$transclude會生成一個新的做用域。默認狀況下,若是咱們簡單使用$transclude(),那麼其做用域就是$transclude 生成的做用域。可是若是咱們使用$transclude($scope, function(clone) {}),那麼做用域就是directive的做用域了。

固然問題又來了,若是咱們想使用父級做用域呢?

$scope.$parent
<div ng-app="app" ng-controller="parentController">
    <div ng-controller="sonController">
        <test-directive url="http://www.tigerbrokers.com">炒美股,上老虎2</test-directive>
    </div>
</div>
angular.module('app', [])
.controller('parentController', function($scope) {
    $scope.title = "PARENT TIGER";
})

.controller('sonController', function($scope) {
    $scope.title = "CHILDREN TIGER";
})

.directive('testDirective', function() {
    return {
        transclude: true,
        replace: true,
        controller: function($scope, $element, $attrs, $transclude, $log) {
            var a = $transclude();
            $element.append(a);
            $log.info($scope.title);
            $log.info($scope.$parent.title);
        }
    }
})

實例地址

10. controllerAs

angualr 1.2以後,就已經開始支持controllerAs的語法,這樣咱們就能夠不用將屬性和方法掛載到$scope上,而是this上。

<div ng-app="app" ng-controller="demoController as demo">{{ demo.name }}</div>
angular.module('app', [])
.controller('demoController', function() {
    this.name = "Jake";
})

實例地址

咱們能夠從實例中看出,這裏的this就是指的as後面的那個別名。而後咱們在表達式中就可使用這個別名

咱們知道,在directive中的controller,主要職能是對外提供交互接口,而結合require與link,所以咱們經常會利用這樣的語法而非上面例子中的$scope

app.directive('testDirective', function() {
    return {
        controller: function() {
            this.name = "Jake";
        }
    }
})

11. require

字符串或者數組,字符串表明另外一個指令的名字,它將做爲link函數的第四個參數。具體用法咱們能夠舉例子來講明。

假設如今咱們要編寫兩個指令,兩個指令的link函數中存在許多重合的方法,這時候咱們就能夠將這些重複的方法寫在第三個指令的controller中,而後在這兩個指令中,使用require將第三個指令引入進來,而後咱們就能夠經過link鏈接函數的第四個參數引用這些重合的方法了

指令之間經常經過這種方式進行交互

<div ng-app="expanderModule" ng-controller="SomeController" class="wrap">
    <accordion>
        <expander class='expander' ng-repeat='expander in expanders' expander-title='expander.title'>
            {{expander.text}}
        </expander>
    </accordion>
</div>
var expModule=angular.module('expanderModule',[])
expModule.directive('accordion', function() {
    return {
        restrict : 'EA',
        replace : true,
        transclude : true,
        template : '<div ng-transclude></div>',
        controller : function() {
            var expanders = [];
            this.gotOpened = function(selectedExpander) {
                angular.forEach(expanders, function(expander) {
                    if (selectedExpander != expander) {
                        expander.showMe = false;
                    }
                });
            }
            this.addExpander = function(expander) {
                expanders.push(expander);
            }
        }
    }
});

expModule.directive('expander', function() {
    return {
        restrict : 'EA',
        replace : true,
        transclude : true,
        require : '^?accordion',
        scope : {
            title : '=expanderTitle'
        },
        template : '<div>'
                   + '<div class="title" ng-click="toggle()">{{title}}</div>'
                   + '<div class="body" ng-show="showMe" ng-transclude></div>'
                   + '</div>',
        link : function(scope, element, attrs, accordionController) {
            scope.showMe = false;
            accordionController.addExpander(scope);
            scope.toggle = function toggle() {
                scope.showMe = !scope.showMe;
                accordionController.gotOpened(scope);
            }
        }
    }
});

expModule.controller("SomeController",function($scope) {
    $scope.expanders = [{
        title : 'Click me to expand',
        text : 'Hi there folks, I am the content that was hidden but is now shown.'
    }, {
        title : 'Click this',
        text : 'I am even better text than you have seen previously'
    }, {
        title : 'Test',
        text : 'test'
    }];
});

實例地址

實例說明,controller是用來與不一樣指令之間通訊的。

另外咱們能夠在require的參數值加上下面的某個前綴,這回改變查找控制器的行爲。

  • 沒有前綴 指令會在自身提供的控制器中進行查找,若是找不到任何控制器,則會拋出一個error
  • ? 若在當前指令中沒有找到所需的控制器,則會將null傳給link連接函數的第四個參數
  • ^ 若是在當前的指令中沒有找到所需的控制器,則會查找父元素的控制器
  • ?^ 若是在當前和父元素中都沒有找到控制器,則會將null傳給link

12. angular指令編譯過程

首先加載angular庫,查找ng-app指令,從而找到應用的邊界,根據ng-app劃定的做用域來調用$compile服務進行編譯。

angular會遍歷整個html文檔,並根據js中指令的定義來處理在頁面上聲明的各個指令,按照指令的優先級排列,根據指令中的配置參數(template, replace, transclude等)轉換DOM,而後就開始按照順序執行各指令的compile函數(若是指令上有定義compile函數)對模板自身進行轉換。

此處的compile函數值什麼指令中配置的,與上面說的$compile服務不同

每一個compile函數執行完後會返回一個link函數,全部的link函數會合成一個大的link函數,而後這個大的link函數就會被執行, 主要作數據綁定,經過 DOM上註冊監聽器來動態修改scope中的數據,或者是使用$watchs監聽scope中變量來修改DOM,從而創建雙向綁定等等。

若咱們的指令中沒有配置compile函數,那咱們配置的link函數就會運行,她作的事情大體跟上面compile返回以後全部的link合成的大link函數差很少。

因此,在指令中compile與link選項是互斥的,若是同時設置了這兩項,那麼就會把compile所返回的函數當作是連接函數,而link選項自己會被忽略。

13. compile編譯函數與link連接函數

cmopile選項能夠返回一個對象或者函數,在這裏咱們能夠在指令和實時數據被放到DOM以前進行DOM操做,好比咱們能夠在這裏進行添加或者刪除節點的DOM操做。

因此編譯函數是負責對模板的DOM進行轉換,而且僅僅只會運行一次

//compile函數的語法
compile:function compile(tElement,tAttrs,transclude){
      return{
        pre:function preLink(scope,iElement,iAttrs,controller){},
        post:function postLink(scope,iElement,iAttrs,controller){}
      }
    }

對於咱們編寫的大部分指令來講,並不須要對模板進行轉換,因此大部分狀況只須要編寫link函數就行。

preLink函數會在編譯階段以後,指令連接到子元素以前執行,相似的,postLink會在指令連接到子元素以後執行。這意味着,爲了避免破壞綁定過程,若是你須要修改DOM結構,你應該在postLink函數中來作這件事情。

link函數負責將做用域與DOM進行連接

//link連接函數
link:function postLink(scope,iElement,iAttrs){}

若指令中定義有require選項,則link函數會有第四個參數,表明控制器或者所依賴的指令的控制器(上面require選項例子已有例子)

內容都真夠多的,終於整理完了,今天寫了兩篇文章,感受好累!若是你們以爲文章對你有幫助,求贊求收藏。

許多內容都是從不一樣的網站上整理而來,每個實例都是本身運行事後保存在codepen上,你們能夠結合codepen的例子結合理解文章內容,本文屬於收集文,非原創,但絕對乾貨!值!得!一!贊!與收藏。

clipboard.png

相關文章
相關標籤/搜索