天天看點

說說AngularJs——自定義指令(三)

自定義指令學習有段時間了,學了些紙上談兵的東西,還沒有真正的寫個指令出來呢。。。是以,随着學習的接近尾聲,本篇除了介紹剩餘的幾個參數外,還将動手結合使用各參數,寫個真正能用的指令出來玩玩。

  我們在自定義指令(上)中,寫了一個簡單的<say-hello></say-hello>,能夠跟美女打招呼。但是看看人家ng内置的指令,都是這麼用的:ng-model=”m”,ng-repeat=”a in array”,不單單是作為屬性,還可以指派給它,與作用域中的一個變量綁定好,内容就可以動态變化了。假如我們的sayHello可以這樣用:<say-hello speak=”content”>美女</say-hello>,把要對美女說的話寫在一個變量content中,然後隻要在controller中修改content的值,頁面就可以顯示對美女說的不同的話。這樣就靈活多了,不至于見了美女隻會說一句hello,然後就沒有然後了。

  為了實作這樣的功能,我們需要使用scope參數,下面來介紹一下。

使用scope為指令劃分作用域

  顧名思義,scope肯定是跟作用域有關的一個參數,它的作用是描述指令與父作用域的關系,這個父作用域是指什麼呢?想象一下我們使用指令的場景,頁面結構應該是這個樣子:

<div ng-controller="testC">
    <say-hello speak="content">美女</say-hello>
</div>
           

外層肯定會有一個controller,而在controller的定義中大體是這個樣子:

var app = angular.module('MyApp', [], function(){console.log('here')});
app.controller('testC',function($scope){
$scope.content = '今天天氣真好!';
});
           

所謂sayHello的父作用域就是這個名叫testC的控制器所管轄的範圍,指令與父作用域的關系可以有如下取值:

取值 說明
false 預設值。使用父作用域作為自己的作用域
true 建立一個作用域,該作用域繼承父作用域
javascript對象 與父作用域隔離,并指定可以從父作用域通路的變量

  乍一看取值為false和true好像沒什麼差別,因為取值為true時會繼承父作用域,即父作用域中的任何變量都可以通路到,效果跟直接使用父作用域差不多。但細細一想還是有差別的,有了自己的作用域後就可以在裡面定義自己的東西,與跟父作用域混在一起是有本質上的差別。好比是父親的錢你想花多少花多少,可你自己掙的錢父親能花多少就不好說了。你若想看這兩個作用域的差別,可以在link函數中列印出來看看,還記得link函數中可以通路到scope吧。

  最有用的還是取值為第三種,一個對象,可以用鍵值來顯式的指明要從父作用域中使用屬性的方式。當scope值為一個對象時,我們便建立了一個與父層隔離的作用域,不過也不是完全隔離,我們可以手工搭一座橋梁,并放行某些參數。我們要實作對美女說各種話就得靠這個。使用起來像這樣:

scope: {
        attributeName1: 'BINDING_STRATEGY',
        attributeName2: 'BINDING_STRATEGY',...
}
           

鍵為屬性名稱,值為綁定政策。等等!啥叫綁定政策?最讨厭冒新名詞卻不解釋的行為!别急,聽我慢慢道來。

  先說屬性名稱吧,你是不是認為這個attributeName1就是父作用域中的某個變量名稱?錯!其實這個屬性名稱是指令自己的模闆中要使用的一個名稱,并不對應父作用域中的變量。好難懂啊。。。可能這麼說太不負責任了,稍後的例子中我們來說明。再來看綁定政策,它的取值按照如下的規則:

符号 說明 舉例
@ 傳遞一個字元串作為屬性的值. str : ‘@string’
= 使用父作用域中的一個屬性,綁定資料到指令的屬性中. name : ‘=username’
& 使用父作用域中的一個函數,可以在指令中調用 getName : ‘&getUserName’

  總之就是用符号字首來說明如何為指令傳值。你肯定迫不及待要看例子了,我們結合例子看一下,小二,上栗子~

舉例說明

  我想要實作上面想像的跟美女多說點話的功能,即我們給sayHello指令加一個屬性,通過給屬性指派來動态改變說話的内容。主要代碼如下:

app.controller('testC',function($scope){
        $scope.content = '今天天氣真好!';
    });
app.directive('sayHello',function(){
    return {
        restrict : 'E',
        template : '<div>hello,<b ng-transclude></b>,{­{cont}­}</div>',
        replace : true,
        transclude : true,
        scope : {
             cont : '=speak'
         }
    };
});
           

然後在模闆中,我們如下使用指令:

<div ng-controller="testC">
    <say-hello speak="content">美女</say-hello>
</div>
           

執行的流程是這樣的:

  ① 指令被編譯的時候會掃描到template中的{ {cont} },發現是一個表達式;

  ② 查找scope中的規則:通過speak與父作用域綁定,方式是傳遞父作用域中的屬性;

  ③ speak與父作用域中的content屬性綁定,找到它的值“今天天氣真好!”

  ④ 将content的值顯示在模闆中

  這樣我們說話的内容cont就跟父作用域綁定到了一其,如果動态修改父作用域的content的值,頁面上的内容就會跟着改變,正如你點選“換句話”所看到的一樣。

  這個例子也太小兒科了吧!簡單雖簡單,但可以讓我們了解清楚,為了檢驗你是不是真的明白了,可以思考一下如何修改指令定義,能讓sayHello以如下兩種方式使用:

<span say-hello speak="content">美女</span>

<span say-hello="content" >美女</span>

  答案我就不說了,簡單的很。下面有更重要的事情要做,我們說好了要寫一個真正能用的東西來着。接下來就結合所學到的東西來寫一個折疊菜單,即點選可展開,再點選一次就收縮回去的菜單

控制器及指令的代碼如下:

app.controller('testC',function($scope){
        $scope.title = '個人簡介';
        $scope.text = '大家好,我是一名前端工程師,我正在研究AngularJs,歡迎大家與我交流,Email:[email protected]';
    });
    app.directive('expander',function(){
        return {
            restrict : 'E',
            templateUrl : 'expanderTemp.html',
            replace : true,
            transclude : true,
            scope : {
                mytitle : '=etitle'
            },
            link : function(scope,element,attris){
                scope.showText = false;
                scope.toggleText = function(){
                    scope.showText = ! scope.showText;
                }
            }
        };
    });
           

HTML中的代碼如下:

<script type="text/ng-template" id="expanderTemp.html">
            <div  class="mybox">
                <div class="mytitle" ng-click="toggleText()">
                {­{mytitle}­}
                </div>
                <div ng-transclude ng-show="showText">
                </div>
            </div>
        </script>

        <div ng-controller="testC">
            <expander etitle="title">{­{text}­}</expander>
        </div>
           

還是比較容易看懂的,我隻做一點必要的解釋。首先我們定義模闆的時候使用了ng的一種定義方式<script type=”text/ng-template” id="expanderTemp.html">,在指令中就可以用templateUrl根據這個id來找到模闆。指令中的{­{mytitle}­}表達式由scope參數指定從etitle傳遞,etitle指向了父作用域中的title。為了實作點選标題能夠展開收縮内容,我們把這部分邏輯放在了link函數中,link函數可以通路到指令的作用域,我們定義showText屬性來表示内容部分的顯隐,定義toggleText函數來進行控制,然後在模闆中綁定好。 如果把showText和toggleText定義在controller中,作為$scope的屬性呢?顯然是不行的,這就是隔離作用域的意義所在,父作用域中的東西除了title之外通通被屏蔽。

  上面的例子中,scope參數使用了=号來指定擷取屬性的類型為父作用域的屬性,如果我們想在指令中使用父作用域中的函數,使用&符号即可,是同樣的原理。

  以上是本人對scope的了解,另外有一篇文章對Angular作用域的解釋也比較詳細,有興趣可以參考http://www.angularjs.cn/A09C。

使用controller和require進行指令間通信

  使用指令來定義一個ui元件是個不錯的想法,首先使用起來友善,隻需要一個标簽或者屬性就可以了,其次是可複用性高,通過controller可以動态控制ui元件的内容,而且擁有雙向綁定的能力。當我們想做的元件稍微複雜一點,就不是一個指令可以搞定的了,就需要指令與指令的協作才可以完成,這就需要進行指令間通信。

  想一下我們進行子產品化開發的時候的原理,一個子產品暴露(exports)對外的接口,另外一個子產品引用(require)它,便可以使用它所提供的服務了。ng的指令間協作也是這個原理,這也正是自定義指令時controller參數和require參數的作用。

  controller參數用于定義指令對外提供的接口,它的寫法如下:

controller: function controllerConstructor($scope, $element, $attrs, $transclude)
           

它是一個構造器函數,将來可以構造出一個執行個體傳給引用它的指令。為什麼叫controller(控制器)呢?其實就是告訴引用它的指令,你可以控制我。至于可以控制那些東西呢,就需要在函數體中進行定義了。先看controller可以使用的參數,作用域、節點、節點的屬性、節點内容的遷移,這些都可以通過依賴注入被傳進來,是以你可以根據需要隻寫要用的參數。關于如何對外暴露接口,我們在下面的例子來說明。

  require參數便是用來指明需要依賴的其他指令,它的值是一個字元串,就是所依賴的指令的名字,這樣架構就能按照你指定的名字來從對應的指令上面尋找定義好的controller了。不過還稍稍有點特别的地方,為了讓架構尋找的時候更輕松些,我們可以在名字前面加個小小的字首:^,表示從父節點上尋找,使用起來像這樣:require : ‘^directiveName’,如果不加,$compile服務隻會從節點本身尋找。另外還可以使用字首:?,此字首将告訴$compile服務,如果所需的controller沒找到,不要抛出異常。

  所需要了解的知識點就這些,接下來是例子時間,依舊是從書上抄來的一個例子,我們要做的是一個手風琴菜單,就是多個折疊菜單并列在一起,此例子用來展示指令間的通信再合适不過。

  首先我們需要定義外層的一個結構,起名為accordion,代碼如下:

app.directive('accordion',function(){
        return {
            restrict : 'E',
            template : '<div ng-transclude></div>',
            replace : true,
            transclude : true,
            controller :function(){
                var expanders = [];
                this.gotOpended = function(selectedExpander){
                    angular.forEach(expanders,function(e){
                        if(selectedExpander != e){
                            e.showText = false;
                        }
                    });
                }

                this.addExpander = function(e){
                    expanders.push(e);
                }
            }
        }
    });
           

需要解釋的隻有controller中的代碼,我們定義了一個折疊菜單數組expanders,并且通過this關鍵字來對外暴露接口,提供兩個方法。gotOpended接受一個selectExpander參數用來修改數組中對應expander的showText屬性值,進而實作對各個子菜單的顯隐控制。addExpander方法對外提供向expanders數組增加元素的接口,這樣在子菜單的指令中,便可以調用它把自身加入到accordion中。

  看一下我們的expander需要做怎樣的修改呢:

app.directive('expander',function(){
        return {
            restrict : 'E',
            templateUrl : 'expanderTemp.html',
            replace : true,
            transclude : true,
            require : '^?accordion',
            scope : {
                title : '=etitle'
            },
            link : function(scope,element,attris,accordionController){
                scope.showText = false;
                accordionController.addExpander(scope);
                scope.toggleText = function(){
                    scope.showText = ! scope.showText;
                    accordionController.gotOpended(scope);
                }
            }
        };
    });
           

首先使用require參數引入所需的accordion指令,添加?^字首表示從父節點查找并且失敗後不抛出異常。然後便可以在link函數中使用已經注入好的accordionController了,調用addExpander方法将自己的作用域作為參數傳入,以供accordionController通路其屬性。然後在toggleText方法中,除了要把自己的showText修改以外,還要調用accordionController的gotOpended方法通知父層指令把其他菜單給收縮起來。

  指令定義好後,我們就可以使用了,使用起來如下:

<accordion>
  <expander ng-repeat="expander in expanders" etitle="expander.title">{­{expander.text}­}</expander>
</accordion>
           

外層使用了accordion指令,内層使用expander指令,并且在expander上用ng-repeat循環輸出子菜單。請注意這裡周遊的數組expanders可不是accordion中定義的那個expanders,如果你這麼認為了,說明還是對作用域不夠了解。此expanders是ng-repeat的值,它是在外層controller中的,是以,在testC中,我們需要添加如下資料:

$scope.expanders = [
            {title: '個人簡介',
             text: '大家好,我是一名前端工程師,我正在研究AngularJs,歡迎大家與我交流,Email:[email protected]'},
            {title: '我的愛好',
             text: '運動類:籃球、足球、乒乓球。  電腦類:前端技術、打DOTA。  其他類:欣賞美女'},
            {title: '性格及工作',
             text: '追求完美主義的處女座極品男人就是我啦~嚴重的代碼潔癖以及對垃圾代碼的零容忍!希望通過自己的努力進入理想的公司工作。'}
        ];
           

繼續閱讀