《AngularJS》Directive(指令)機制詳解

爲何使用AngularJS 指令?

使用過 AngularJS 的朋友應該最感興趣的是它的指令。現今市場上的前端框架也只有AngularJS 擁有自定義指令的功能,而且AngularJS 是目前惟一提供Web應用可複用能力的框架。css

目前有不少JavaScript 產品提供插件給Web開發人員。例如, Bootstrap 就是當前比較流行的提供樣式和JavaScript插件的前端開發工具包。可是開發人員在使用Booostrap中的插件時, 必須切換到JavaScript 模式來寫 jQuery 代碼來激活插件雖然jQuery 代碼寫起來十分簡單,可是必須和HTML進行同步,這是一個單調乏味且容易出錯的過程。html

AngularJS主頁展現了一個簡單的例子,用於實現Bootstrap中的 Tab功能,能夠在頁面中輕鬆添加 Tab 功能,而且使用方法和 ul 標籤同樣簡單。HTML代碼以下:前端

<body ng-app="components"> 
  <h3>BootStrap Tab Component</h3> 
  <tabs> 
    <pane title="First Tab"> 
      <div>This is the content of the first tab.</div> 
    </pane> 
    <pane title="Second Tab"> 
      <div>This is the content of the second tab.</div> 
    </pane> 
  </tabs> 
</body>
複製代碼

 

JavaScript代碼以下:jquery

複製代碼
angular.module('components', []). 
  directive('tabs', function() { 
    return { 
      restrict: 'E', 
      transclude: true, 
      scope: {}, 
      controller: [ "$scope", function($scope) { 
        var panes = $scope.panes = []; 
  
        $scope.select = function(pane) { 
          angular.forEach(panes, function(pane) { 
            pane.selected = false; 
          }); 
          pane.selected = true; 
        } 
  
        this.addPane = function(pane) { 
          if (panes.length == 0) $scope.select(pane); 
          panes.push(pane); 
        } 
      }], 
      template: 
        '<div class="tabbable">' + 
          '<ul class="nav nav-tabs">' + 
            '<li ng-repeat="pane in panes" ng-class="{active:pane.selected}">'+ 
              '<a href="" ng-click="select(pane)">{{pane.title}}</a>' + 
            '</li>' + 
          '</ul>' + 
          '<div class="tab-content" ng-transclude></div>' + 
        '</div>', 
      replace: true 
    }; 
  }). 
  directive('pane', function() { 
    return { 
      require: '^tabs', 
      restrict: 'E', 
      transclude: true, 
      scope: { title: '@' }, 
      link: function(scope, element, attrs, tabsCtrl) { 
        tabsCtrl.addPane(scope); 
      }, 
      template: 
        '<div class="tab-pane" ng-class="{active: selected}" ng-transclude>' + 
        '</div>', 
      replace: true 
    }; 
  })
複製代碼

 

你能夠從如下連接查看效果:http://jsfiddle.net/powertoolsteam/GBE7N/1/git

image

正如你所見,除了擁有用於實現指令的 <tabs> 和<pane> 標籤,頁面和常規HTML頁面沒有什麼區別。HTML開發人員無需編寫任何代碼。固然,總須要有第一個吃螃蟹的人,建立指令共享使用,可是目前Tabs指令已經很常見了,能夠在任何地方複用(如BootStrap,、jQueryUIWijmo, 和一些知名的前端插件集)。angularjs

因爲指令的易用和易編寫,許多用戶已經開始使用AngularJS編寫指令了。例如, AngularJS 開發組已經基於AngularJS實現了一系列指令-UI Bootstrap 來代替Bootstrap; 知名ComponentOne 控件廠商也在AngularJS 基礎上建立了Wijmo ;咱們也能夠在GitHub上找到一些公共指令資料庫:jQueryUI widgetsgithub

擁有了 AngularJS,是否是以爲本身已經站在了巨人的肩膀上了?可是不要高興的太早,若是已經有了這麼多的指令供咱們使用,那咱們爲何還要學習AngularJS ,爲何還要學習自定義指令呢?bootstrap

舉個簡單的例子,也許你有特殊的需求:假設你在一家財務公司工做,你須要建立一張財務表單,它須要以表格的形式展現數據、擁有綁定、編輯、校驗而且同步數據更新到服務器的功能。表單插件很常見可是可以知足這些具體需求的不得而知了,因此你必須根據實際業務需求來建立自定義指令。數組

複製代碼
<body ng-app="abcFinance"> 
  <h3>Offshore Investment Summary</h3> 
  <abc-investment-form 
    customer="currentCustomer" 
    country="currentCountry"> 
  </abc-investment-form data> 
</body> 
複製代碼

 

這就是本篇文章的目的,接下來咱們會討論如何建立 AngularJS指令。瀏覽器

 

建立自定義AngularJS 指令

文章開頭的自定義指令十分的簡單。它僅僅實現了同步的功能。通常指令是包含更多元素的:

複製代碼
//建立指令模塊 (或者檢索現有模塊) 
var m = angular.module("myApp");

// 建立"my-dir"指令 
myApp.directive("myDir", function() { 
  return { 
    restrict: "E",        // 指令是一個元素 (並不是屬性) 
    scope: {              // 設置指令對於的scope 
      name: "@",          // name 值傳遞 (字符串,單向綁定) 
      amount: "=",        // amount 引用傳遞(雙向綁定) 
      save: "&"           // 保存操做 
    }, 
    template:             // 替換HTML (使用scope中的變量) 
      "<div>" + 
      "  {{name}}: <input ng-model='amount' />" + 
      "  <button ng-click='save()'>Save</button>" + 
      "</div>", 
    replace: true,        // 使用模板替換原始標記 
    transclude: false,    // 不復制原始HTML內容 
    controller: [ "$scope", function ($scope) { …  }], 
    link: function (scope, element, attrs, controller) {…} 
  } 
});  
複製代碼

 

效果以下:

image


注意這個自定義指令遵循一種格式:以"my" 爲前綴,相似於命名空間,所以若是你在應用中引用了多個模塊指令,你能夠經過前綴很容易的判斷出它是在哪定義的。這不是硬性要求,可是這樣作能夠帶來不少便利。

指令的構造函數會返回帶有屬性的JavaScript 對象。這些內容在AngularJS 主頁中都有清晰說明。如下是我對一些屬性的理解:

  1. restrict: 說明指令在HTML中的應用形式,備選項有"A"、"E" 和 "C", "M" ,分別表明 attribute、element、class和comment(默認值爲"A")。咱們將更多的關注attributes-如何建立UI元素。
  2. scope: 建立指令的做用範圍,scope在指令中做爲屬性標籤傳遞。Scope 是建立能夠複用指令的必要條件,每一個指令(不管是處於嵌套指令的哪一級)都有其惟一的做用域,它不依賴於父scope。scope 對象定義names 和types 變量。上面的例子即建立了3個scope變量。
    • name: "@" (值傳遞,單向綁定): 
      "@"符號表示變量是值傳遞。指令會檢索從父級scope中傳遞而來字符串中的值。指令可使用該值但沒法修改,是最經常使用的變量。
    • amount: "=" (引用,雙向綁定) 
      "="符號表示變量是引用傳遞。指令檢索主Scope中的引用取值。值能夠是任意類型的,包括複合對象和數組。指令能夠更改父級Scope中的值,因此當指令須要修改父級Scope中的值時咱們就須要使用這種類型。
    • save: "&" (表達式) 
      「&」符號表示變量是在父級Scope中啓做用的表達式。它容許指令實現比修改值更高級的操做。
  3. template: 替代原始模板中的標記的字符串。替換功能將替換全部舊元素爲新值。注意template是如何使用Scope中定義的變量的。這容許你無需寫任何額外的代碼便可建立macro-style 風格指令。replace: 說明是否替換原始標記中的值或是追加原始標記中的值。默認值是false,這時原始標記將被保留。
  4. transclude: 說明自定義指令是否複製原始標記中的內容。例如,以前展現的「tab」指令設置了transclude 爲 true,由於tab 元素包含其餘HTML 元素。 "dateInput" 指令則須要在初始化時爲空,因此須要設置transclude 爲false。
  5. link: 該方法在指令中扮演着重要的角色。它負責執行DOM 操做和註冊事件監聽器等。link 方法包含如下參數:
    • scope: 指令Scope的引用。scope 變量在初始化時是不被定義的,link 方法會註冊監視器監視值變化事件。
    • element: 包含指令的DOM元素的引用, link 方法通常經過jQuery 操做實例(若是沒有加載jQuery,還可使用Angular's jqLite )。
    • controller: 在有嵌套指令的狀況下使用。這個參數做用在於把子指令的引用提供給父指令,容許指令之間進行交互, tab 指令就是使用該參數較典型的例子:http://jsfiddle.net/powertoolsteam/GBE7N/1/

注意,當調用link 方法時, 經過值傳遞("@")的scope 變量將不會被初始化,它們將會在指令的生命週期中另外一個時間點進行初始化,若是你須要監聽這個事件,可使用scope.$watch 方法。 

AngularJS 指令實例講解

1.一點小說明

 

指令的做用:實現語義化標籤

 

咱們經常使用的HTML標籤是這樣的:

 

<div>
    <span>一點點內容</span>
</div>

 

而使用AngularJS的directive(指令)機制,咱們能夠實現這樣的東西:

 

<tabpanel>
    <panel>子面板1</panel>
    <panel>子面板2</panel>
</tabpanel>

 

不少人可能要驚呼,這貨和JSP或者Struts等等框架裏面的taglib很像啊!

 

呃,說實話,實際上就是這樣的,只不過這裏是使用JavaScript來實現的。正由於如此,因此不少taglib作不到的功能,使用它就均可以作到,好比訪問N層scope裏面的對象之類的事情(參見後面第5個例子)。

 

2.實例1:從最簡單的開始

 

<html ng-app='app'>
    <body>
        <hello></hello>
    </body>

    <script src="../angular-1.0.3/angular.min.js"></script>
    <script src="HelloDirect.js"></script>
</html>

 

對於以上代碼裏面的<hello>標籤,瀏覽器顯然是不認識的,它惟一能作的事情就是無視這個標籤。那麼,爲了讓瀏覽器可以認識這個標籤,咱們須要使用Angular來定義一個hello指令(本質上說就是本身來把<hello>這種玩意兒替換成瀏覽器能識別的那些標準HTML標籤)。

 

來看這段舒適的JS代碼:

 

var appModule = angular.module('app', []);
appModule.directive('hello', function() {
    return {
        restrict: 'E',
        template: '<div>Hi there</div>',
        replace: true
    };
});

 

以上代碼大概看兩眼就能夠了,不要太在乎細節。

 

而後咱們就能夠在瀏覽器裏面看到這樣的內容:

 



 

 

實際產生的標籤結構是這樣的:

 



 

 

能夠看到,<hello>這個東東已經被<div>Hi there</div>這個標籤替換掉了,這也是以上JS代碼裏面replace:true這行配置的做用,代碼裏面的template配置 項固然就是咱們要的div標籤啦,至於restrict:'E'這個配置項的含義,請看下錶:

 



 

 

ok,看完上面的表格,對於restrict這個屬性相信你已經秒懂了,那麼咱們來玩兒點花樣吧。若是咱們須要替換的HTML標籤很長,顯然不能用 拼接字符串的方式來寫,這時候咱們能夠用templateUrl來替代template,從而能夠把模板寫到一個獨立的HTML文件中。

 

3.實例2:transclude(變換)

 

先看例子,JS代碼:

 

var appModule = angular.module('app', []);
    appModule.directive('hello', function() {
    return {
        restrict: 'E',
        template: '<div>Hi there <span ng-transclude></span></div>',
        transclude: true
    };
});

 

HTML代碼:

 

<html ng-app='app'>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    </head>
    <body>
        <hello>
            <br/>
            <span>原始的內容,</span><br/>
            <span>還會在這裏。</span>
        </hello>
        <hello>
        </hello>
    </body>

    <script src="../angular-1.0.3/angular.min.js"></script>
    <script src="Transclude.js"></script>
</html>

 

運行效果以下:

 



 

 

生成的HTML標籤結構以下:

 



 

 

和第一個例子對比,這個例子的JS和HTML代碼都略有不一樣,JS代碼裏面多了一個transclude: true,HTML代碼裏面在<hello>內部出現了子標籤。

 

按照咱們在第一個例子中的說法,指令的做用是把咱們自定義的語義化標籤替換成瀏覽器可以認識的HTML標籤。那好,若是咱們自定義的標籤內部出現了子標籤,應該如何去處理呢?很顯然,transclude就是用來處理這種狀況的。

 

對於當前這個例子,transclude的做用能夠簡化地理解成:把<hello>標籤替換成咱們所編寫的HTML模板,可是<hello>標籤內部的內容保持不變

 

很顯然,因爲咱們沒有加replace:true選項,因此<hello>標籤還在,沒有被替換掉。同時,經過這個例子你還還會發現一 個暗藏的屬性,那就是瀏覽器實際上很是智能,雖然它並不認識<hello>這個標籤,可是頁面沒有出錯,它只是默默地把這個標籤忽略掉了!怎 麼樣?是否是碉堡了?

 

你能夠本身在上面的JS代碼裏面加上replace:true,而後再看生成的HTML結構。

 

4.實例3:關於compile和link

 

JS代碼:

 

var appModule = angular.module('app', []);
appModule.directive('hello', function() {
    return {
        restrict: 'E',
        template: '<span>Hi there</span>',
        replace: true
    };
});
appModule.controller('MyController',function($scope) {
    $scope.things = [1,2,3,4,5,6];
});

 

HTML代碼:

 

<html ng-app='app'>
    <body ng-controller='MyController'>
        <div ng-repeat='thing in things'>
            {{thing}}.<hello></hello>
        </div>
    </body>

    <script src="../angular-1.0.3/angular.min.js"></script>
    <script src="CompileAndLink.js"></script>
</html>

 

呃,這個例子是用來解釋一點點理論的,因此單純看效果可能看不出個鳥。

 

如前所述,指令的本質實際上是一個替換過程。好,既然如此,Angular究竟是如何進行替換的呢?嗯嗯,這個過程分2個階段,也就是本節標題所說的compile(編譯)和link(鏈接)了。

 

簡而言之,compile階段進行標籤解析和變換,link階段進行數據綁定等操做。這裏面更加細節的處理過程請參見《AngularJS》這本書中的解析,這裏就不贅述了(呃,其實是由於解釋起來很長很麻煩,叔懶得在這兒說了

 )。

 

那麼,知道這件事情有什麼用途呢?

 

比方說,你有一些事件須要綁定到某個元素上,那麼你須要提供一個link函數,作法請看下一個例子。

 

5.實例4:一個複雜一點的例子Expander

 

這是《AngularJS》這本書裏面提供的一個例子,可是書裏面沒有給出完整的可運行代碼,因此這裏給出來,你們參考一下。

 

JS代碼:

 

var expanderModule=angular.module('expanderModule', [])
expanderModule.directive('expander', function() {
    return {
        restrict : 'EA',
        replace : true,
        transclude : true,
        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) {
            scope.showMe = false;
            scope.toggle = function toggle() {
                scope.showMe = !scope.showMe;
            }
        }
    }
});
expanderModule.controller('SomeController',function($scope) {
    $scope.title = '點擊展開';
    $scope.text = '這裏是內部的內容。';
});

 

HTML代碼:

 

<html ng-app='expanderModule'>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <script src="../angular-1.0.3/angular.min.js"></script>
        <link rel="stylesheet" type="text/css" href="ExpanderSimple.css"/>
    </head>
    <body>
        <div ng-controller='SomeController'>
            <expander class='expander' expander-title='title'>
                {{text}}
            </expander>
        </div>
    </body>
    <script src="ExpanderSimple.js"></script>
</html>

 

CSS代碼:

 

.expander {
    border: 1px solid black;
    width: 250px;
}

.expander>.title {
    background-color: black;
    color: white;
    padding: .1em .3em;
    cursor: pointer;
}

.expander>.body {
    padding: .1em .3em;
}

 

運行效果以下:

 



 

 

注意一下JS代碼裏面的這一段:

 

link : function(scope, element, attrs) {
    scope.showMe = false;
    scope.toggle = function toggle() {
        scope.showMe = !scope.showMe;
    }
}

 

本身跑一跑例子,研究一番,很少解釋。

 

6.實例5:一個綜合的例子

 

JS代碼:

 

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'
    }];
});

 

HTML代碼:

 

<html ng-app="expanderModule">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <script src="../angular-1.0.3/angular.min.js"></script>
        <link rel="stylesheet" type="text/css" href="Accordion.css"/>
    </head>
    <body ng-controller='SomeController' >
        <accordion>
            <expander class='expander' ng-repeat='expander in expanders' expander-title='expander.title'>
                {{expander.text}}
            </expander>
        </accordion>
    </body>
    <script src="Accordion.js"></script>
</html>

 

CSS代碼:

 

.expander {
    border: 1px solid black;
    width: 250px;
}

.expander>.title {
    background-color: black;
    color: white;
    padding: .1em .3em;
    cursor: pointer;
}

.expander>.body {
    padding: .1em .3em;
}

 

運行效果:

 



 

 

這個例子主要的難點在於如何在子Expander裏面訪問外層Accordion的scope中的數據,這一點解釋起來略複雜,這裏就不展開了,詳細描述參見《AngularJS》一書 

相關文章
相關標籤/搜索