angular指令中使用ngModelController


在這篇文章中 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM