1. 程式人生 > >ngModel 值不更新 介面不展示(轉自部落格園雪狼部落格)

ngModel 值不更新 介面不展示(轉自部落格園雪狼部落格)

ngular中的$scope是頁面(view)和資料(model)之間的橋樑,它連結了頁面元素和model,也是angular雙向繫結機制的核心。

而ngModel是angular用來處理表單(form)的最重要的指令,它連結了頁面表單中的可互動元素和位於$scope之上的model,它會自動把ngModel所指向的model值渲染到form表單的可互動元素上,同時也會根據使用者在form表單的輸入或互動來更新此model值。

在原始碼中,model值的格式化、解析、驗證都是由ngModel指令所對應的控制器ngModelController來實現的。

在筆者所維護的國內ng群中,經常被問到一個問題:

為什麼我的ng-model=“xxx”值不能在頁面顯示了呢?

對於ngModel的這類問題主要分為兩類:

  • model值不滿足表單驗證條件,所以angular不會渲染它
  • 由於JavaScript特殊的原型鏈繼承機制,對$scope中屬性的賦值並不能更新到父$scope

在本節中,我們將會詳細分析此類問題,藉此深入剖析ngModel的工作原理。

驗證引起的model值不顯示

我們先來看一個修改商品數量的例子,要求為必須輸入1-100的個數;

下面是對應的html程式碼:

<body class="container">
  <div ng-controller="DemoCtrl as demo">
   <div ng-form="form" class="form-horizontal">
      <div class="form-group" ng-class="{'has-error': form.amount.$invalid }">
      <label for="amount">Amount</label>
      <!-- 這個input將工作不正常 -->
    <input id="amount" name="amount" type="number" ng-model="demo.amount" class="form-control" placeholder="1 - 100" min="1" max="100"/>
    </div>
  </div>
   </div>
</body>

javascript程式碼:

angular.module("com.ngbook.demo", [])
    .controller("DemoCtrl", [function(){
    var vm = this;

    vm.amount = 0;

    return vm;
}]);

在程式碼中我們已經為ngModel變數amount賦值了整數“0”,可是介面顯示效果仍然顯示”1 – 100”的placeholder(如下圖)。

下面是關於angular number元件ngModel轉換函式程式碼:

var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;

function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
    textInputType(scope, element, attr, ctrl, $sniffer, $browser);

    ctrl.$parsers.push(function(value) {
        var empty = ctrl.$isEmpty(value);
        if (empty || NUMBER_REGEXP.test(value)) {
            ctrl.$setValidity('number', true);
            return value === '' ? null : (empty ? value : parseFloat(value));
        } else {
            ctrl.$setValidity('number', false);
            return undefined;
        }
    });

    addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState);

    ctrl.$formatters.push(function(value) {
        return ctrl.$isEmpty(value) ? '' : '' + value;
    });

    if (attr.min) {
        var minValidator = function(value) {
            var min = parseFloat(attr.min);
            return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value);
        };

        ctrl.$parsers.push(minValidator);
        ctrl.$formatters.push(minValidator);
    }

    if (attr.max) {
        var maxValidator = function(value) {
            var max = parseFloat(attr.max);
            return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value);
        };

        ctrl.$parsers.push(maxValidator);
        ctrl.$formatters.push(maxValidator);
    }

    ctrl.$formatters.push(function(value) {
        return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value);
    });
}

ngModel作為angular雙向繫結中的重要組成部分,負責view控制元件互動資料到$scope上model的同步。當然這裡存在一些差異,view上的顯示和輸入都是字串型別,而在model上的資料則是有特定型別的,如常用的int、float、Date、Array、Object等。ngModel為了實現資料到model的型別轉換,在ngModelController中提供了兩個管道陣列$formatters和$parsers,它們分別是將model的資料轉換為view互動控制元件顯示的值和將互動控制元件得到的view值轉換為model資料,它們都是一個數組物件,在ngModel啟動資料轉換時,會以UNIX管道式傳遞執行這一些列的轉換。我們也可以手動的新增$formatters和$parsers的轉換函式(unshift、push),當然在這裡也是做資料驗證的最佳時機,能夠轉換意味應該是合法的資料。

在number元件程式碼中,我們清晰看見:依次添加了對數字驗證轉換、最小值合法性驗證、最大值合法驗證。首先會啟動$parsers轉換,如果在轉換過程中出現不合法驗證則會設定ngModelController.$setValidity驗證錯誤,則返回undefined。對於model資料到互動控制元件顯示,同樣也會經過$formatters轉換管道,對於沒有通過驗證的邏輯,同樣也會ngModelController.$setValidity設定驗證錯誤,返回undefined,因此這不合法的model資料不會顯示在互動控制元件上。

原型鏈繼承問題

JavaScript中每個物件都會連結到一個原型物件,並且他可以從中繼承屬性。即使通過字面量建立的物件也會連結到Object.prototype,它是JavaScript中的標配物件。JavaScript的原型鏈繼承相對於其他語言常見的繼承,是一種另類的繼承,它是實施於物件上的動態繼承方式,而非常見的實施與型別class之上的靜態繼承體系。JavaScript的這種繼承方式很靈活,一個物件可以被多個物件繼承,而且他們共享同一例項物件,但理解起來顯得格外複雜,從JavaScript原型和原型鏈可以看出它的複雜性。在Javascript中,每個函式都有一個原型屬性prototype指向自身的原型,而由這個函式建立的物件也有一個proto屬性指向這個原型,而函式的原型是一個物件,所以這個物件也會有一個proto指向自己的原型,這樣逐層深入直到Object物件的原型,這樣就形成了原型鏈。下面的是JavaScript原型繼承基礎原型和原型鏈展示圖。

函式是由Function函式建立的物件,因此函式也有一個proto屬性指向Function函式的原型。需要注意的是,真正形成原型鏈的是每個物件的proto屬性,而不是函式的prototype屬性。更多的內容關於原型和原型鏈的知識,請參考《Javascript模式》這本書。

JavaScript的原型鏈連線只在屬性檢索的時候才會啟用,如果我們嘗試去獲取物件的某個屬性值,但該物件沒有此屬性名,則JavaScript會試著從原型物件中獲取該屬性值。如果那個物件也沒有該屬性名,那麼在繼續從它的原型中尋找,依次類推,直到Object.prototype,如果仍然沒有找到該屬性值,則返回結果為undefined。不幸的是,這種原型鏈連線檢索,只會在屬性檢索的的時候啟用,並不會在更新屬性值時啟用,因此當我們對於基礎型別(非引用物件上的屬性,換句通俗的話來說,就是不會出現“.”運算子)的屬性更新的時候,它並不能更新父物件的屬性,替代方式是在自身物件上建立了該屬性。這也是angular中對於基礎型別的屬性,不能在子controller中被修改的原因,導致在子controller中ngModel的更新並不會反應在父controller上。

下邊是關於該問題的一個簡化例子:

HTML:

<div ng-controller="ParentCtrl">
    <div class="form-group">
        <h4>Parent Controller:</h4>
        <pre></pre>
        <input type="text" ng-model="greet" class="form-control" />
    </div>
    <div ng-controller="ChildCtrl">
        <div class="form-group">
            <h4>Child controller:</h4>
            <pre></pre>
            <input type="text" ng-model="greet" class="form-control" />
        </div>
    </div>
</div>

JavaScript:

angular.module("com.ngbook.demo", [])
    .controller("ParentCtrl", ["$scope", function($scope) {

        $scope.greet = "hello angular!";

    }])
    .controller("ChildCtrl", angular.noop);

從初始化顯示效果中,我們能看出子$scope之繼承了來自父$scope的greet屬性,都顯示為”hello angular!“。如果我們嘗試利用父controller提供了input控制元件改變父$scope的greet屬性,你也能看見子controller區域的顯示也會被及時更新。對於ngController預設會使用原型鏈繼承其父物件的屬性,所有的$scope的根$scope或稱祖$scope是來自ngApp節點建立的$rootScope,換句話說,$rootScope是萬物之源,所有的$scope都直接或者間接繼承至它。

當我們嘗試去改變輸入框的greet屬性的時,則發生了下面的情況:子controller區域發生了更新,父controller區域卻無法更新。因為上面所說的JavaScript的原型鏈檢索並不對更新啟用,對於基礎型別JavaScript在自身物件(這裡是子$scope)上建立了一個同名的變數。你也想可以從下面angular除錯外掛batarang截圖中看出來。一旦利用子controller的input控制元件修改了greet屬性,再次之後我再次嘗試修改父controller區域的greet屬性,子controller區別不會在像初始化時候那樣及時同步了,它們之間完全獨立了,各自擁有了自己的greet屬性。

batarang外掛截圖

經過上面的例子分析,相信作為讀者的你已經能夠理解這類由於繼承鏈引用問題導致的ngModel不能更新問題了,請記住:這是JavaScript原型繼承的issue,並不是angular的issue。

那麼我們在子controller中如何更新父controller的屬性值呢?這個問題已經很簡單了,issue的問題在於沒有啟用原型鏈的檢索,那麼如果我們將ngModel的屬性變為引用物件,換句話說:在ngModel的屬性值中加了“.”,那麼在JavaScript的原型鏈檢索就會啟動了。

HTML:

<div ng-controller="ParentCtrl">
    <div class="form-group">
        <h4>Parent Controller:</h4>
        <pre></pre>
        <input type="text" ng-model="vm.greet" class="form-control" />
    </div>
    <div ng-controller="ChildCtrl">
        <div class="form-group">
            <h4>Child controller:</h4>
            <pre></pre>
            <input type="text" ng-model="vm.greet" class="form-control" />
        </div>
    </div>
</div>

JavaScript:

angular.module("com.ngbook.demo", [])
    .controller("ParentCtrl", ["$scope", function($scope) {

        $scope.vm = {
            greet: "hello angular!"
        };

    }])
    .controller("ChildCtrl", angular.noop);

這裡在ngModel屬性值多引入了“vm”變數,這個時候,不管我們嘗試修改greet值,整個頁面都會得到相應的同步。關於這個問題,作者更推薦使用angular 1.2後的controller as vm的方式解決,更多的資訊請閱讀《使用controller as vm方式.md》一節。

作者:破  狼
出處:http://www.cnblogs.com/whitewolf/