AngularJS中的指令實(shí)踐開發(fā)指南(二)
在AngularJS中的指令實(shí)踐指南(一)中給大家介紹了,如何隔離一個(gè)指令的scope。第二部分將承接上一篇繼續(xù)介紹。首先,我們會(huì)看到在使用隔離scope的情況下,如何從指令內(nèi)部訪問(wèn)到父scope的屬性。接著,我們會(huì)基于對(duì) controller 函數(shù)和 transclusions 討論如何為指令選擇正確的scope。這篇文章的最后會(huì)以通過(guò)一個(gè)完整的記事本應(yīng)用來(lái)實(shí)踐指令的使用。
隔離scope和父scope之間的數(shù)據(jù)綁定
通常,隔離指令的scope會(huì)帶來(lái)很多的便利,尤其是在你要操作多個(gè)scope模型的時(shí)候。但有時(shí)為了使代碼能夠正確工作,你也需要從指令內(nèi)部訪問(wèn)父scope的屬性。好消息是Angular給了你足夠的靈活性讓你能夠有選擇性的通過(guò)綁定的方式傳入父scope的屬性。讓我們重溫一下我們的 helloWorld 指令,它的背景色會(huì)隨著用戶在輸入框中輸入的顏色名稱而變化。還記得當(dāng)我們對(duì)這個(gè)指令使用隔離scope的之后,它不能工作了嗎?現(xiàn)在,我們來(lái)讓它恢復(fù)正常。
假設(shè)我們已經(jīng)初始化完成app這個(gè)變量所指向的Angular模塊。那么我們的 helloWorld 指令如下面代碼所示:
app.directive('helloWorld', function() { return { scope: {}, restrict: 'AE', replace: true, template: '<p style="background-color:{{color}}">Hello World</p>', link: function(scope, elem, attrs) { elem.bind('click', function() { elem.css('background-color','white'); scope.$apply(function() { scope.color = "white"; }); }); elem.bind('mouseover', function() { elem.css('cursor', 'pointer'); }); } }; });
使用這個(gè)指令的HTML標(biāo)簽如下:
<body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color"/> <hello-world/> </body>
上面的代碼現(xiàn)在是不能工作的。因?yàn)槲覀冇昧艘粋€(gè)隔離的scope,指令內(nèi)部的 {{color}} 表達(dá)式被隔離在指令內(nèi)部的scope中(不是父scope)。但是外面的輸入框元素中的 ng-model 指令是指向父scope中的 color 屬性的。所以,我們需要一種方式來(lái)綁定隔離scope和父scope中的這兩個(gè)參數(shù)。在Angular中,這種數(shù)據(jù)綁定可以通過(guò)為指令所在的HTML元素添加屬性和并指令定義對(duì)象中配置相應(yīng)的 scope 屬性來(lái)實(shí)現(xiàn)。讓我們來(lái)細(xì)究一下建立數(shù)據(jù)綁定的幾種方式。
選擇一:使用 @ 實(shí)現(xiàn)單向文本綁定
在下面的指令定義中,我們指定了隔離scope中的屬性 color 綁定到指令所在HTML元素上的參數(shù) colorAttr。在HTML標(biāo)記中,你可以看到 {{color}}表達(dá)式被指定給了 color-attr 參數(shù)。當(dāng)表達(dá)式的值發(fā)生改變時(shí),color-attr 參數(shù)也跟著改變。隔離scope中的 color 屬性的值也相應(yīng)地被改變。
app.directive('helloWorld', function() { return { scope: { color: '@colorAttr' }, .... // the rest of the configurations }; });
更新后的HTML標(biāo)記代碼如下:
<body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color"/> <hello-world color-attr="{{color}}"/> </body>
我們稱這種方式為單項(xiàng)綁定,是因?yàn)樵谶@種方式下,你只能將字符串(使用表達(dá)式{{}})傳遞給參數(shù)。當(dāng)父scope的屬性變化時(shí),你的隔離scope模型中的屬性值跟著變化。你甚至可以在指令內(nèi)部監(jiān)控這個(gè)scope屬性的變化,并且觸發(fā)一些任務(wù)。然而,反向的傳遞并不工作。你不能通過(guò)對(duì)隔離scope屬性的操作來(lái)改變父scope的值。
注意點(diǎn):
當(dāng)隔離scope屬性和指令元素參數(shù)的名字一樣是,你可以更簡(jiǎn)單的方式設(shè)置scope綁定:
app.directive('helloWorld', function() { return { scope: { color: '@' }, .... // the rest of the configurations }; });
相應(yīng)使用指令的HTML代碼如下:
<hello-world color="{{color}}"/>
選擇二:使用 = 實(shí)現(xiàn)雙向綁定
讓我們將指令的定義改變成下面的樣子:
app.directive('helloWorld', function() { return { scope: { color: '=' }, .... // the rest of the configurations }; });
相應(yīng)的HTML修改如下:
<body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color"/> <hello-world color="color"/> </body>
與 @ 不同,這種方式讓你能夠給屬性指定一個(gè)真實(shí)的scope數(shù)據(jù)模型,而不是簡(jiǎn)單的字符串。這樣你就可以傳遞簡(jiǎn)單的字符串、數(shù)組、甚至復(fù)雜的對(duì)象給隔離scope。同時(shí),還支持雙向的綁定。每當(dāng)父scope屬性變化時(shí),相對(duì)應(yīng)的隔離scope中的屬性也跟著改變,反之亦然。和之前的一樣,你也可以監(jiān)視這個(gè)scope屬性的變化。
選擇三:使用 & 在父scope中執(zhí)行函數(shù)
有時(shí)候從隔離scope中調(diào)用父scope中定義的函數(shù)是非常有必要的。為了能夠訪問(wèn)外部scope中定義的函數(shù),我們使用 &。比如我們想要從指令內(nèi)部調(diào)用 sayHello() 方法。下面的代碼告訴我們?cè)撛趺醋觯?/p>
app.directive('sayHello', function() { return { scope: { sayHelloIsolated: '&' }, .... // the rest of the configurations }; });
相應(yīng)的HTML代碼如下:
<body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color"/> <say-hello sayHelloIsolated="sayHello()"/> </body>
這個(gè) Plunker 例子對(duì)上面的概念做了很好的詮釋。
父scope、子scope以及隔離scope的區(qū)別
作為一個(gè)Angular的新手,你可能會(huì)在選擇正確的指令scope的時(shí)候感到困惑。默認(rèn)情況下,指令不會(huì)創(chuàng)建一個(gè)新的scope,而是沿用父scope。但是在很多情況下,這并不是我們想要的。如果你的指令重度地使用父scope的屬性、甚至創(chuàng)建新的屬性,會(huì)污染父scope。讓所有的指令都使用同一個(gè)父scope不會(huì)是一個(gè)好主意,因?yàn)槿魏稳硕伎赡苄薷倪@個(gè)scope中的屬性。因此,下面的這個(gè)原則也許可以幫助你為你的指令選擇正確的scope。
1.父scope(scope: false) – 這是默認(rèn)情況。如果你的指令不操作父scoe的屬性,你就不需要一個(gè)新的scope。這種情況下是可以使用父scope的。
2.子scope(scope: true) – 這會(huì)為指令創(chuàng)建一個(gè)新的scope,并且原型繼承自父scope。如果你的指令scope中的屬性和方法與其他的指令以及父scope都沒有關(guān)系的時(shí)候,你應(yīng)該創(chuàng)建一個(gè)新scope。在這種方式下,你同樣擁有父scope中所定義的屬性和方法。
3.隔離scope(scope:{}) – 這就像一個(gè)沙箱!當(dāng)你創(chuàng)建的指令是自包含的并且可重用的,你就需要使用這種scope。你在指令中會(huì)創(chuàng)建很多scope屬性和方法,它們僅在指令內(nèi)部使用,永遠(yuǎn)不會(huì)被外部的世界所知曉。如果是這樣的話,隔離的scope是更好的選擇。隔離的scope不會(huì)繼承父scope。
Transclusion(嵌入)
Transclusion是讓我們的指令包含任意內(nèi)容的方法。我們可以延時(shí)提取并在正確的scope下編譯這些嵌入的內(nèi)容,最終將它們放入指令模板中指定的位置。 如果你在指令定義中設(shè)置 transclude:true,一個(gè)新的嵌入的scope會(huì)被創(chuàng)建,它原型繼承子父scope。 如果你想要你的指令使用隔離的scope,但是它所包含的內(nèi)容能夠在父scope中執(zhí)行,transclusion也可以幫忙。
假設(shè)我們注冊(cè)一個(gè)如下的指令:
app.directive('outputText', function() { return { transclude: true, scope: {}, template: '<div ng-transclude></div>' }; });
它使用如下:
<div output-text> <p>Hello {{name}}</p> </div>
ng-transclude 指明在哪里放置被嵌入的內(nèi)容。在這個(gè)例子中DOM內(nèi)容 <p>Hello {{name}}</p> 被提取和放置到 <div ng-transclude></div> 內(nèi)部。有一個(gè)很重要的點(diǎn)需要注意的是,表達(dá)式{{name}}所對(duì)應(yīng)的屬性是在父scope中被定義的,而非子scope。你可以在這個(gè)Plunker例子中做一些實(shí)驗(yàn)。如果你想要學(xué)習(xí)更多關(guān)于scope的知識(shí),可以閱讀這篇文章。
transclude:'element' 和 transclude:true的區(qū)別
有時(shí)候我我們要嵌入指令元素本身,而不僅僅是它的內(nèi)容。在這種情況下,我們需要使用 transclude:'element'。它和 transclude:true 不同,它將標(biāo)記了 ng-transclude 指令的元素一起包含到了指令模板中。使用transclusion,你的link函數(shù)會(huì)獲得一個(gè)名叫 transclude 的鏈接函數(shù),這個(gè)函數(shù)綁定了正確的指令scope,并且傳入了另一個(gè)擁有被嵌入DOM元素拷貝的函數(shù)。你可以在這個(gè) transclude 函數(shù)中執(zhí)行比如修改元素拷貝或者將它添加到DOM上等操作。 類似 ng-repeat 這樣的指令使用這種方式來(lái)重復(fù)DOM元素。仔細(xì)研究一下這個(gè)Plunker,它使用這種方式復(fù)制了DOM元素,并且改變了第二個(gè)實(shí)例的背景色。
同樣需要注意的是,在使用 transclude:'element'的時(shí)候,指令所在的元素會(huì)被轉(zhuǎn)換成HTML注釋。所以,如果你結(jié)合使用 transclude:'element' 和 replace:false,那么指令模板本質(zhì)上是被添加到了注釋的innerHTML中——也就是說(shuō)其實(shí)什么都沒有發(fā)生!相反,如果你選擇使用 replace:true,指令模板會(huì)替換HTML注釋,那么一切就會(huì)如果所愿的工作。使用 replade:false 和 transclue:'element'有時(shí)候也是有用的,比如當(dāng)你需要重復(fù)DOM元素但是并不想保留第一個(gè)元素實(shí)例(它會(huì)被轉(zhuǎn)換成注釋)的情況下。對(duì)這塊還有疑惑的同學(xué)可以閱讀stackoverflow上的這篇討論,介紹的比較清晰。
controller 函數(shù)和 require
如果你想要允許其他的指令和你的指令發(fā)生交互時(shí),你需要使用 controller 函數(shù)。比如有些情況下,你需要通過(guò)組合兩個(gè)指令來(lái)實(shí)現(xiàn)一個(gè)UI組件。那么你可以通過(guò)如下的方式來(lái)給指令添加一個(gè) controller 函數(shù)。
app.directive('outerDirective', function() { return { scope: {}, restrict: 'AE', controller: function($scope, $compile, $http) { // $scope is the appropriate scope for the directive this.addChild = function(nestedDirective) { // this refers to the controller console.log('Got the message from nested directive:' + nestedDirective.message); }; } }; });
這個(gè)代碼為指令添加了一個(gè)名叫 outerDirective 的controller。當(dāng)另一個(gè)指令想要交互時(shí),它需要聲明它對(duì)你的指令 controller 實(shí)例的引用(require)??梢酝ㄟ^(guò)如下的方式實(shí)現(xiàn):
app.directive('innerDirective', function() { return { scope: {}, restrict: 'AE', require: '^outerDirective', link: function(scope, elem, attrs, controllerInstance) { //the fourth argument is the controller instance you require scope.message = "Hi, Parent directive"; controllerInstance.addChild(scope); } }; });
相應(yīng)的HTML代碼如下:
<outer-directive> <inner-directive></inner-directive> </outer-directive>
require: ‘^outerDirective' 告訴Angular在元素以及它的父元素中搜索controller。這樣被找到的 controller 實(shí)例會(huì)作為第四個(gè)參數(shù)被傳入到 link 函數(shù)中。在我們的例子中,我們將嵌入的指令的scope發(fā)送給父親指令。如果你想嘗試這個(gè)代碼的話,請(qǐng)?jiān)陂_啟瀏覽器控制臺(tái)的情況下打開這個(gè)Plunker。同時(shí),這篇Angular官方文檔上的最后部分給了一個(gè)非常好的關(guān)于指令交互的例子,是非常值得一讀的。
一個(gè)記事本應(yīng)用
這一部分,我們使用Angular指令創(chuàng)建一個(gè)簡(jiǎn)單的記事本應(yīng)用。我們會(huì)使用HTML5的 localStorage 來(lái)存儲(chǔ)筆記。最終的產(chǎn)品在這里,你可以先睹為快。
我們會(huì)創(chuàng)建一個(gè)展現(xiàn)記事本的指令。用戶可以查看他/她創(chuàng)建過(guò)的筆記記錄。當(dāng)他點(diǎn)擊 add new 按鈕的時(shí)候,記事本會(huì)進(jìn)入可編輯狀態(tài),并且允許創(chuàng)建新的筆記。當(dāng)點(diǎn)擊 back 按鈕的時(shí)候,新的筆記會(huì)被自動(dòng)保存。筆記的保存使用了一個(gè)名叫 noteFactory 的工廠類,它使用了 localStorage。工廠類中的代碼是非常直接和可理解的。所以我們就集中討論指令的代碼。
第一步
我們從注冊(cè) notepad 指令開始。
app.directive('notepad', function(notesFactory) { return { restrict: 'AE', scope: {}, link: function(scope, elem, attrs) { }, templateUrl: 'templateurl.html' }; });
這里有幾點(diǎn)需要注意的:
因?yàn)槲覀兿胱屩噶羁芍赜茫赃x擇使用隔離的scope。這個(gè)指令可以擁有很多與外界沒有關(guān)聯(lián)的屬性和方法。
這個(gè)指令可以以屬性或者元素的方式被使用,這個(gè)被定義在 restrict 屬性中。
現(xiàn)在的link函數(shù)是空的這個(gè)指令從 templateurl.html 中獲取指令模板
第二步
下面的HTML組成了指令的模板。
<div class="note-area" ng-show="!editMode"> <ul> <li ng-repeat="note in notes|orderBy:'id'"> <a href="#" ng-click="openEditor(note.id)">{{note.title}}</a> </li> </ul> </div> <div id="editor" ng-show="editMode" class="note-area" contenteditable="true" ng-bind="noteText"></div> <span><a href="#" ng-click="save()" ng-show="editMode">Back</a></span> <span><a href="#" ng-click="openEditor()" ng-show="!editMode">Add Note</a></span>
幾個(gè)重要的注意點(diǎn):
note 對(duì)象中封裝了 title,id 和 content。
ng-repeat 用來(lái)遍歷 notes 中所有的筆記,并且按照自動(dòng)生成的 id 屬性進(jìn)行升序排序。
我們使用一個(gè)叫 editMode 的屬性來(lái)指明我們現(xiàn)在在哪種模式下。在編輯模式下,這個(gè)屬性的值為 true 并且可編輯的 div 節(jié)點(diǎn)會(huì)顯示。用戶在這里輸入自己的筆記。
如果 editMode 為 false,我們就在查看模式,顯示所有的 notes。
兩個(gè)按鈕也是基于 editMode 的值而顯示和隱藏。
ng-click 指令用來(lái)響應(yīng)按鈕的點(diǎn)擊事件。這些方法將和 editMode 一起添加到scope中。
可編輯的 div 框與 noteText 相綁定,存放了用戶輸入的文本。如果你想編輯一個(gè)已存在的筆記,那么這個(gè)模型會(huì)用它的文本內(nèi)容初始化這個(gè) div 框。
第三步
我們?cè)趕cope中創(chuàng)建一個(gè)名叫 restore() 的新函數(shù),它用來(lái)初始化我們應(yīng)用中的各種控制器。 它會(huì)在 link 函數(shù)執(zhí)行的時(shí)候被調(diào)用,也會(huì)在 save 按鈕被點(diǎn)擊的時(shí)候調(diào)用。
scope.restore = function() { scope.editMode = false; scope.index = -1; scope.noteText = ''; };
我們?cè)?link 函數(shù)的內(nèi)部創(chuàng)建這個(gè)函數(shù)。 editMode 和 noteText 之前已經(jīng)解釋過(guò)了。 index 用來(lái)跟蹤當(dāng)前正在編輯的筆記。 當(dāng)我們?cè)趧?chuàng)建一個(gè)新的筆記的時(shí)候,index 的值會(huì)設(shè)成 -1. 我們?cè)诰庉嬕粋€(gè)已存在的筆記的時(shí)候,它包含了那個(gè) note 對(duì)象的 id 值。
第四步
現(xiàn)在我們要?jiǎng)?chuàng)建兩個(gè)scope函數(shù)來(lái)處理編輯和保存操作。
scope.openEditor = function(index) { scope.editMode = true; if (index !== undefined) { scope.noteText = notesFactory.get(index).content; scope.index = index; } else { scope.noteText = undefined; } }; scope.save = function() { if (scope.noteText !== '') { var note = {}; note.title = scope.noteText.length > 10 ? scope.noteText.substring(0, 10) + '. . .' : scope.noteText; note.content = scope.noteText; note.id = scope.index != -1 ? scope.index : localStorage.length; scope.notes = notesFactory.put(note); } scope.restore(); };
這兩個(gè)函數(shù)有幾點(diǎn)需要注意:
openEditor 為編輯器做準(zhǔn)備工作。如果我們?cè)诰庉嬕粋€(gè)筆記,它會(huì)獲取當(dāng)前筆記的內(nèi)容并且通過(guò)使用 ng-bind 將內(nèi)容更新到可編輯的 div 中。
如果我們?cè)趧?chuàng)建一個(gè)新的筆記,我們會(huì)將 noteText 設(shè)置成 undefined,以便當(dāng)我們?cè)诒4婀P記的時(shí)候,觸發(fā)相應(yīng)的監(jiān)聽器。
如果 index 參數(shù)是 undefined,它表明用戶正在創(chuàng)建一個(gè)新的筆記。
save 函數(shù)通過(guò)使用 notesFactory 來(lái)存儲(chǔ)筆記。在保存完成后,它會(huì)刷新 notes 數(shù)組,從而監(jiān)聽器能夠監(jiān)測(cè)到筆記列表的變化,來(lái)及時(shí)更新。
save 函數(shù)調(diào)用在重置 controllers 之后調(diào)用restore(),從而可以從編輯模式進(jìn)入查看模式。
第五步
在 link 函數(shù)執(zhí)行時(shí),我們初始化 notes 數(shù)組,并且為可編輯的 div 框綁定一個(gè) keydown 事件,從而保證我們的 nodeText 模型與 div 中的內(nèi)容保持同步。我們使用這個(gè) noteText 來(lái)保存我們的筆記內(nèi)容。
var editor = elem.find('#editor'); scope.restore(); // initialize our app controls scope.notes = notesFactory.getAll(); // load notes editor.bind('keyup keydown', function() { scope.noteText = editor.text().trim(); });
第六步
最后,我們?cè)贖TML如同使用其他的HTML元素一樣使用我們的指令,然后開始做筆記吧。
<h1 class="title">The Note Making App</h1> <notepad/>
總結(jié)
一個(gè)很重要的點(diǎn)需要注意的是,任何使用jQuery能做的事情,我們都能用Angular指令來(lái)做到,并且使用更少的代碼。所以,在使用jQuery之前,請(qǐng)考慮一下我們能否在不進(jìn)行DOM操作的情況下以更好的方式來(lái)完成任務(wù)。試著使用Angular來(lái)最小化jQuery的使用吧。
再來(lái)看一下我們的筆記本應(yīng)用,刪除筆記的功能被故意漏掉了。鼓勵(lì)讀者們自己實(shí)驗(yàn)和實(shí)現(xiàn)這個(gè)功能。 你可以從GitHub上下到這個(gè)Demo的源代碼。
相關(guān)文章
使用angularjs創(chuàng)建簡(jiǎn)單表格
AngularJS提供豐富填寫表單和驗(yàn)證。我們可以用ng-click來(lái)處理AngularJS點(diǎn)擊按鈕事件,然后使用 $dirty 和 $invalid標(biāo)志做驗(yàn)證的方式。使用novalidate表單聲明禁止任何瀏覽器特定的驗(yàn)證。下面我們來(lái)看看如何使用angularjs創(chuàng)建簡(jiǎn)單表格2016-01-01Angular5.0 子組件通過(guò)service傳遞值給父組件的方法
這篇文章主要介紹了Angular5.0 子組件通過(guò)service傳遞值給父組件的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07AngularJs返回前一頁(yè)面時(shí)刷新一次前面頁(yè)面的方法
今天小編就為大家分享一篇AngularJs返回前一頁(yè)面時(shí)刷新一次前面頁(yè)面的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-10-10Angular學(xué)習(xí)筆記之a(chǎn)ngular的$filter服務(wù)淺析
本文是小編記錄的angular學(xué)習(xí)筆記,通過(guò)本文首先給大家介紹了$filter服務(wù),然后介紹下內(nèi)置filter及filter的簡(jiǎn)單使用,非常不錯(cuò)具有參考借鑒價(jià)值,感興趣的朋友一起看看吧2016-11-11Angular8 簡(jiǎn)單表單驗(yàn)證的實(shí)現(xiàn)示例
這篇文章主要介紹了Angular8 簡(jiǎn)單表單驗(yàn)證的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06AngularJS 依賴注入詳解和簡(jiǎn)單實(shí)例
本文主要介紹AngularJS 依賴注入,這里對(duì)依賴注入做了詳細(xì)介紹講解,并提供效果圖和示例代碼以便學(xué)習(xí)參考2016-07-07詳解angularjs中如何實(shí)現(xiàn)控制器和指令之間交互
本篇文章主要介紹了詳解angularjs中如何實(shí)現(xiàn)控制器和指令之間交互,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05