在這篇文章中 angular學習筆記(三十)-指令(10)-require和controller 說到了通過require屬性和controller參數來讓指令與指令之間互相交互.
本篇主要介紹的是指令與ngModel指令的交互.也就是說,ngModel指令雖然是內置的,但它也有自己的controller屬性,其它指令也可以通過require來得到ngModel指令的controller屬性的實例來與ngModel指令進行交互.
ngModelController用在什么場合呢?我們知道,ngModel提供了數據綁定,驗證,樣式更新,數據格式化,編譯功能,但是它故意沒有提供和邏輯相關的處理,比如視圖的重新渲染和監聽dom事件.這些和邏輯處理相關的dom,就應該使用ngModelController來進行數據綁定.
ngModelController的方法和屬性很多...無法一一列舉,有些也很少用,這里會重點講一下常用的幾個(帶有*的):
方法:
*1. $render()
這個方法會在視圖需要被更新的時候調用. 比如以下這些場景:
$rollbackViewValue()被調用. 如果我們把視圖值回滾到數據模型的值時,$render()會被調用.關於$rollbackViewValue()這個方法,詳見此文:angular-1.3 之ng-model-options指令- ng-model綁定的值被程序改變了,並且$modelValue和$viewValue都和上一次不同了.
由於ng-model沒有深度對比模型的變化.什么叫沒有深度對比模型的變化: 也就是angular學習筆記(十四)-$watch(1)這里提到的第三個參數ifDeep,ng-model內置的對比機制,相當於這里的ifDeep是false,不進行深度對比.所以$render()只在$modelValue和$viewValue都發生了實際的改變, 才會會被調用.什么叫實際的改變? 就是說,如果$modelValue或者$viewValue是一個對象,而不是一個字符串或者數字,那么,這個對象中的一個屬性值發生了變化,這不算真正的變化,因為它對象的引用地址沒有發生變化,它指向的還是同一個對象.
*2. $isEmpty(value)
當我們需要判斷input的value值是否為空的時候,可以使用這個方法.
value是必須要傳的,注意它判斷的是value值是否為空,而不是ngModel綁定的那個數據模型的值.其實可以就當它是個判斷是否為空的方法,傳入一個參數,判斷這個參數是否為空,你傳入任何值都可以.只是說,一般情況下都會把ngModel綁定的那個值傳給它.
你可以自己在指令里重寫這個方法,來定義自己所需要的'是否為空'的概念.比如用在一個類型為checkbox的input元素上,因為當checkbox的值為false的時候,$isEmpty()的結果就是empty.
如果值是undefined,null,'',或者NaN,則返回true,否則返回false.
3. $setValidity(validationErrorKey, isValid);
4. $setPristine()
把元素設置到原始狀態.移除元素的ng-dirty類名,添加ng-pristine類名.
5. $setDirty()
把元素設置到臟值模式.移除元素的ng-pristine類型,添加ng-dirty類名。
6.$setUntouched()
把元素設置到沒有觸碰過的狀態.移除ng-touched類名.添加ng-untouched類名.
7.$setTouched()
把元素設置到觸碰過的狀態.移除ng-untouched類名,添加ng-touched類名.
8.$rollbackViewValue()
參考:angular-1.3 之ng-model-options指令
9.$validate()
10.$commitViewValue()
把一個未發生的更新提交給$modelValue.
在使用ng-model-options指令的時候,input元素可能正在等待某個事件的觸發,來同步一個將要發生的更新.這個方法很少用,因為ngModelController通常在事件響應中自動處理了這件事.
*11.$setViewValue(value, trigger)
更新視圖值.當一個input的指令元素想要改變視圖值的時候,這個方法會被調用.這通常是dom元素內部的事件來處理的.最典型的例子就是
在input中輸入值,會改變Hello后面的視圖的值.原因就是input的輸入事件會調用$setViewValue方法.類似的還有select元素.
如果value是一個對象,而不是一個字符串或者數值,那我們應該在傳入$setViewValue之前先拷貝一份.因為ngModel不會深度監測對象的變化,它只看對象的引用地址是否發生了變化.如果你僅僅改變了對象的某個屬性,ngModel不會意識到它已經改變了,也不會去經過$parsers和$validators管道.
因此,當對象被傳入到$setViewValue函數里以后,你不能再改變它的屬性值,否則可能引起當前scope下的模型值被錯誤地改變.
當$setViewValue被調用時,新的value將會通過$parsers和$validators管道后被提交. 如果沒有配置ngModelOptions,那么value直接進入處理流程,最后它被應用到$modelValue和ng-model綁定的屬性表達式上.
還有一點,所有添加在$viewChangeListeners這個數組里的函數,都會被執行.
在使用了ngModelOptions的情況下,上面的說法不適用.上面說到的這些行為都會被等待直到dom元素的updateOn事件觸發.同樣,如果定義了debounce延遲,那么這些行為也會在延遲時間到了以后才發生.
需要注意,執行$setViewValue()方法,不會觸發$digest.
這里的trigger是做什么的,不太明白...
屬性:
*1.$viewValue
指令元素的視圖中實際的值.注意,它一定等於 $setViewValue(value)的value值
*2.$modelValue
ngModel綁定的數據模型的模型值.它不一定等於 $setViewValue(value)的value值,在$setViewValue(value, trigger)里面提到的使用了ngModelOptions時,比如雖然調用了$setViewValue,但是因為設置了ngModelOptions的debounce屬性,所以它會延遲,等到同步的時候,value值才會被設置到$modelValue上.
3.$parsers
一個數組.數組里的元素是函數. $setViewValue(value)被賦值給$modelValue之前,value值首先會經過$parsers里的所有函數,每次將返回值傳遞給下一個函數.最后才被賦值到$modelValue.在這個過程中就包括了驗證和轉換.對於驗證這個步驟,它會使用$setValidity這個方法,驗證失敗的將返回undefined.
4.$formatters
一個數組.數組里的元素是函數. 和$parsers一樣,它也是管道.當模型值發生變化的時候被調用.模型值會倒着調用數組中的函數,然后把返回值傳給下一個函數,最后返回的值就會被傳遞給dom元素.用來在視圖中格式化模型值:
一個將小寫轉換為大寫的格式化方法:
function formatter(value) { if (value) { return value.toUpperCase(); } } ngModel.$formatters.push(formatter);
*5. $validators
一個json對象.
{ validateName: function(modelValue,viewValue){ return ... } }
當$setViewValue(value)被賦值給$modelValue之前,會經過$parsers管道,經過$parsers管道時,就會經過這個$validators管道.其中validateName是驗證的名字,函數是這個驗證的方法,其中的參數modelValue和viewValue就是$modelValue和$viewValue,如果返回值是true,則通過validateName
驗證,如果返回值是false,則沒有通過validateName驗證,如果沒有通過validateName驗證,$error.validateName就會為true.這就是angular內部驗證表單項的原理.
eg: 自定義一個驗證規則,輸入內容中必須包含數字
<div class="alert alert-danger" role="alert" ng-show="myForm.myWidget.$error.validCharacters"> <strong>Oh!</strong> 不符合自定義的驗證規則! </div>
ngModel.$validators.validCharacters = function(modelValue, viewValue) { var value = modelValue || viewValue; return /[0-9]+/.test(value); };
*6.$asyncValidators
一個json對象.用來處理異步驗證(比如一個http請求).
{ validateName: function(modelValue,viewValue){ return promise } }
其中validateName是驗證的名字,函數是這個驗證的方法,其中的參數modelValue和viewValue就是$modelValue和$viewValue,返回值必須是一個promise對象,如果這個promise對象傳遞給它下一個.then方法失敗通知,則不通過validateName驗證,如果這個promise對象傳遞給它下一個.then方法成功通知,則表示通過validateName驗證.當異步驗證開始執行的時候,所有的異步驗證都是平行並發的.只有當所有的驗證都通過時,數據模型才會被同步更新.只要有一個異步驗證沒有完成,這個驗證名就會被放到ngModelController的$pending屬性中.另外,所有的異步驗證都只會在所有的同步驗證通過以后才開始.
核心代碼:
<input validate-name type="text" name="myWidget" ng-model="userContent" ng-model-options="{updateOn:'blur'}" class="form-control" required uniqueUsername> <div class="alert alert-danger" role="alert" ng-show="myForm.myWidget.$error.uniqueUsername"> <strong>Oh!</strong> 已經存在的用戶名! </div>
app.directive('validateName',function($http,$q){
return {
restrict:'A',
require:'?^ngModel',
link:function(scope,iele,iattr,ctrl){
ctrl.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;
// 異步驗證用戶名是否已經存在
return $http.get('/api/users/' + value).
then(function resolved(res) {
if(res.data){
//用戶名已經存在,驗證失敗,給下一個promise傳遞失敗通知.
return $q.reject('res.data');
}
else {
//用戶名不存在,驗證成功.
return true
}
}, function rejected() {
//請求失敗
})
};
}
}
});
異步驗證比較重要,所以我會另外開一篇文章來舉例詳解:angular中的表單數據異步驗證
7.$viewChangeListeners
一個數組,數組中的元素都是函數.這些函數在視圖發生改變的時候被執行,不帶有什么參數,也不需要返回值.在第11條方法里說到,在不使用ngModelOptions延遲時調用$setViewValue的時候,他們就會執行.
*8.$error
json對象. 這個很簡單,用到很多次了.就是所有驗證失敗的驗證名和失敗信息組成的json對象.
*9.$pending
json對象. 第6個屬性里提到過的,正在進行中的異步驗證會被放在這個對象里
10.$untouched
布爾值.如果元素還沒有失去過焦點,那這個值就是true.
11.$touched
布爾值.如果元素已經失去過焦點,那這個值就是false.
12.$pristine
布爾值.如果元素還沒有和用戶發生過交互,那這個值就是true.
13.$dirty
布爾值.如果元素已經和用戶發生過交互,那這個值就是true.
*14.$valid
布爾值.這個也很常用,就是當所有驗證(異步同步),都通過的時候,它就是true
*15.$invalid
布爾值.這個也很常用,就是當所有驗證(異步同步),其中有一個或一個以上驗證失敗,它就是true.
16.$name
字符串.很簡單,就是獲取元素的name屬性.
注意上面說到的這些屬性:
我們這篇文章說的是ngModelController,所以說這些屬性是ngModelController的屬性,但是其中有一部分一般不在ngModelController里面用,比如:$error,$pending,$valid,$invalid,等,這些屬性,我們通常是這樣用的:
<div class="alert alert-danger" role="alert" ng-show="myForm.myWidget.$error.uniqueUsername"> <strong>Oh!</strong> 已經存在的用戶名! </div> <div class="panel-body"> {{myForm.myWidget.$pending}} </div>
但是,你心里要知道,其實他們也是ngModelController的屬性哦~
ngModelController就全部講完了,可能看起來比較混亂...我盡量按照理解的總結一下:
兩個核心的屬性:
$viewValue: 視圖里的值,也就是input輸入框的值,這個值就是$setViewValue(value)中的value.
$modeValue: 數據模型的值
$viewValue會在input事件觸發的時候,被同步到$modelValue.什么叫input事件觸發? 如果我什么都沒有定義,那么,就是ng默認的事件,也就是一邊輸入,就會一邊觸發.如果是定義了ngModelOptions,那就是在自己指定的事件觸發的時候,$viewValue被同步到$modelValue.
$viewValue被同步到$modelValue時,並不是直接就賦值了,而是經過了下面說到的三個核心管道.
雙向數據綁定的那個表達式,比如ng-model='name', 這個scope下的name值,它是和$modelValue保持一致的.所以,當input事件觸發的時候,$modelValue被賦值,name也就在這時被改變為這個值.
兩個核心方法:
$render: 如果模型值被改變,需要同步視圖的值(后台改變了模型值,或者使用了$rollbackViewValue()).也就是說,$render函數負責將模型值同步到視圖上.
$setViewValue: 用於設置視圖值,也就是將input的value值賦值給$viewValue.
需要注意: $render是同步模型值到視圖值,那么同步視圖值到模型值是什么方法呢? 注意angular並沒有為我們提供這樣一個接口.而是在兩個核心屬性里面提到的,當input事件觸發時候,就會把視圖值同步到模型值,但是我們可以自定義視圖值同步到模型值的過程中的三個管道.
三個核心管道:
$parsers: 用於改變視圖值的格式.
$validators: 用於添加自定義的同步驗證.
$asyncValidators: 用於添加自定義的異步驗證.
這個三個管道具體怎么用,看例子.
四個常用屬性:
$error: 用來存儲驗證錯誤
$pending: 用來存儲正在異步驗證中的驗證內容
$valid: 用來存儲表單項是否都通過了驗證.
$invalid: 用來存儲表單項是否都通過了驗證.
這些都是用在html里面,使用myFrom.myWidget...來獲取的...
最后我用一個例子來把這個流程給順一遍:

'請輸入內容'這個文本框其實是個div,可編輯的的div,然后我們通過ngModelController來讓它實現和input一樣的雙向數據綁定的效果.
為了清楚的看到效果,我通過ngModelOptions給它添加了1000毫秒的延遲.
另外,把輸入的內容通過$parsers屬性來進行格式轉換,把小寫的轉換為大寫.
下面來看代碼:
html:
<!DOCTYPE html> <html ng-app="customControl"> <head> <title>ngModelController</title> <meta charset="utf-8"> <script src="../angular-1.3.2.js"></script> <script src="script.js"></script> <link type="text/css" href="../bootstrap.css" rel="stylesheet" /> <style> *{font-family: 'MICROSOFT YAHEI'} </style> </head> <body> <div class="container" ng-controller="ctrl"> <div class="page-header"> <h1>ngModelController- <small>創建一個實現了雙向數據綁定的可編輯文本區域</small></h1> </div> <form role="form" name="myForm"> <div class="form-group"> <div contenteditable name="myWidget" ng-model="userContent" ng-model-options="{debounce:1000}" class="form-control" required default-text="請輸入內容"></div> </div> <div class="form-group"> <button type="button" class="btn btn-default btn-primary" ng-click="setNone()">設置為'抱歉,我沒有想輸入的內容'</button> </div> <div class="alert alert-danger" role="alert" ng-show="myForm.myWidget.$error.required"> <strong>Oh!</strong> 必填! </div> </form> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">用戶輸入的內容為:</h3> </div> <div class="panel-body"> {{userContent}} </div> </div> </div> </body> </html>
它和普通的雙向數據綁定的唯一區別就是,它是一個可編輯div.
然后我們看angularjs是如何處理contenteditable指令的:
var app = angular.module('customControl',[]); app.controller('ctrl',function($scope){ $scope.setNone = function(){ $scope.userContent = '抱歉,我沒有想輸入的內容' } }); app.directive('contenteditable',function(){ return { restrict:'A', require:'?^ngModel', link:function(scope,element,attrs,ngModel){ if(!ngModel){ return } //一開始scope.userContent是空 console.log(ngModel.$isEmpty(scope.userContent)); ngModel.$setViewValue(attrs.defaultText); //這里其實不需要調用的,只是為了演示$isEmpty,不是demo需要 //調用了$setViewValue以后就不為空了,但是如果設置了ngModelOptions,則沒用,因為$setViewValue沒有被賦值給$modelValue. console.log(ngModel.$isEmpty(scope.userContent)); ngModel.$render = function(){ element.html(ngModel.$viewValue || attrs.defaultText) }; element.bind('focus',function(){ if(element.html()==attrs.defaultText){ element.html('') } }); element.bind('focus blur keyup change',function(){ console.log(scope.userContent);
ngModel.$setViewValue(element.html()); console.log('$viewValue為:'+ngModel.$viewValue); console.log('$modelValue為:'+ngModel.$modelValue); }); ngModel.$parsers.push(function(value){ return value.toUpperCase() }) } } });
$isEmpty(value):
這里把userContent傳入,判斷它是否為空,如果這里沒有使用ngModelOptions,那么在調用了$setViewValue以后,userContent就會有值了.但是這里使用了ngModelOptions,所以$setViewValue以后,$viewValue值不會馬上被賦值給$modelValue,而模型值應該是等於$modelValue的,
所以這里得到的兩次結果都是true.
$render():
當模型值變化的時候,這個方法會被調用,也就是當我點擊 "設置為'抱歉,我沒有想輸入的內容'" 按鈕的時候,userContent發生了變化,會調用$render()方法.注意一點,當直接改變userContent的值的時候,$viewValue和$modelValue都會被異步的改變為這個值.改變以后,再調用$render().
$setViewValue():
當用戶輸入的時候,通過$setViewValue改變$viewValue的值, 默認的input ng-model它自己處理了這件事,這里div元素我們手動處理.
$viewValue和$modelValue和綁定值:
這里我ngModelOptions設置延遲了1000毫秒,當我很慢很慢的輸入時,結果如下:


可以看到,當還沒有開始輸入時,$viewValue是'',因為div里面的內容就是'',而$modelValue是undefined
當我開始輸第一個字,$viewValue會立刻變成我輸入的內容(這是$setViewValue的作用),但是$modelValue不會發生變化
當我延遲了1000毫秒以后,再輸入下一個字,$viewValue當然實時同步了,而可以看到,$modelValue也同步了上一次輸入的值.因為已經過了1000毫秒了.
...
值得注意的是,userContent始終是和$modelValue一致的.或者說$modelValue是和userContent一致的.我也不清楚是誰先變化.但可以知道他倆是一致的.
最后延遲1000毫秒后讓鼠標失去焦點,這樣,三個值是完全一致的了.
如果輸的快一點,那就是這樣:

$parsers:
這個很簡單,就是讓$viewValue被賦值給$modelValue的時候經過這個管道,把小寫變成了大寫.
點擊查看效果: http://plnkr.co/edit/CbOS1nFosPDfQXvsGTyR?p=preview
相關閱讀:
ngModelOptionsng(包含ngModel中的$rollbackViewValue方法):
angular-1.3 之ng-model-options指令
ngModel自定義驗證:
參考文獻: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
完整代碼: https://github.com/OOP-Code-Bunny/angular/tree/master/ngModelController
