angular指令中使用ngModelController

詩&遠方發表於2014-11-24

在這篇文章中 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自定義驗證:

angular中的表單資料自定義驗證

 

 

參考文獻: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController  

完整程式碼: https://github.com/OOP-Code-Bunny/angular/tree/master/ngModelController

 

相關文章