復刻對象: KO範例3 - 動態新增下拉選單選項

先示範一個失敗寫法。在KO範例裡,新增選項按鈕不包含在ViewModel範圍內,而是透過jQuery click事件在選項集合新增物件,而選項集合是ko.observabelArray(),KO能感測到新增動作,同步增加下拉選單選項;但同樣做法直接搬到NG行不通,option是尋常JavaScript陣列,NG感測不到Scope之外對ViewModel屬性的更動。如以下程式,按鈕後vm.options陣列雖已加入新元素,卻不會反應到下拉選單。Live Demo

<!DOCTYPE html>
<html ng-app="sampleApp">
<head>
  <meta charset="utf-8">
  <title>Lab 3 - 動態增加SELECT選項(無效)</title>
</head>
<body ng-controller="defaultCtrl">
<select id="selOptions" style="width: 120px"
ng-options="item.text for item in model.options" ng-model="model.result">
</select>
Result=<span ng-bind="model.result.value"></span>
 
<div style="margin-top: 10px">
Text: <input id='txtOptText' value="Firefox" /> 
Value: <input id='txtOptValue' value="ff" /> &nbsp; 
<input type="button" value="新增選項" id='btnAddOpt' />
  <div id="dvDebug"></div>
</div>
  <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
  <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
  <script>
    var vm = null;
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
      function myViewModel() {
        var self = this;
        self.options = [
          { text: "IE", value: "ie" }
        ];
        self.result = self.options[0];
      }
      vm = new myViewModel();
      $scope.model = vm;
    });
    $("#btnAddOpt").click(function() {
      vm.options.push({
        "text": $("#txtOptText").val(),
        "value": $("#txtOptValue").val()          
      });
      $("#dvDebug").text(JSON.stringify(vm.options));
    });
  </script>
</body>
</html>

在下圖中,options陣列已新增Firefox選項,但下拉選單卻仍只有一個選項,由dvDebug顯示的JSON.stringify(vm.options)可以驗證這點:

以上範例突顯了NG與KO的一項重要差異: KO需要明確宣告ko.observable()、ko.observableArray(),但不管任何時候變更這些受觀察物件都會引發UI及相依變數連動;而在NG中,一般的JavaScript物件屬性就可做為繫結對象,但相對地,要"在Scope感應範圍內更動資料,才會引發UI改變及連動"。就這個案例而言,將新增選項動作移入ng-click(),Scope便會將資枓變化反應到<select>。Live Demo 

<input type="button" value="新增選項" id='btnAddOpt' ng-click="model.addOption()" />
</div>
  <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
  <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
  <script>
    var vm = null;
    angular.module("sampleApp", [])
    .controller("defaultCtrl", function($scope) {
      function myViewModel() {
        var self = this;
        self.options = [
          { text: "IE", value: "ie" }
        ];
        self.result = self.options[0];
        self.addOption = function() {
          self.options.push({
            "text": $("#txtOptText").val(),
            "value": $("#txtOptValue").val()
          });
        };
      }
      vm = new myViewModel();
      $scope.model = vm;
    });
  </script>

(說明: 在ViewModel中存取$("#txt…")有違SoC原則,為不良設計。此處強調僅移動function()位置,故函式內部保留原樣)

想從NG事件之外更動ViewModel,需要一些技巧。NG提供jQuery.scope()方法,可以取得UI元素所屬的Scope物件,接著我們就可以存取到model物件及model.options: Live Demo

  <script>
    var vm = null;
    angular.module("sampleApp", [])
      .controller("defaultCtrl", function($scope) {
        function myViewModel() {
          var self = this;
          self.options = [{
            text: "IE",
            value: "ie"
          }];
          self.result = self.options[0];
        }
        vm = new myViewModel();
        $scope.model = vm;
      });
    $("#btnAddOpt").click(function() {
      var scope = $(this).scope();
      scope.model.options.push({
        "text": $("#txtOptText").val(),
        "value": $("#txtOptValue").val()
      });
      $("#dvDebug").text(JSON.stringify(scope.model.options));
    });
  </script>

但是以上程式仍不管用,還差一個關鍵: 必須在Scope監視下更動資料,才會觸發繫結UI元素、連動變數或函數的更新。使用$scope.$apply()執行ViewModel更新,NG才能掌握資料異動。用法有三種: $scope.$apply("指令字串") 、 $scope.$apply(function() { 更新程式碼 }),或是依一般做法更新後再不帶參數呼叫$scopt.$apply()。Live Demo

    $("#btnAddOpt").click(function() {
      var scope = $(this).scope();
      //方法1: 傳入指令字串給$apply()執行
      scope.$apply("model.options.push({text:'Chrome',value:'chrome'})");
      //方法2: 利用$apply()執行函式
      scope.$apply(function() {
        scope.model.options.push({
          "text": $("#txtOptText").val(),
          "value": $("#txtOptValue").val()
        });
      });
      //方法3: 執行完畢呼叫$apply()[不帶參數]
      scope.model.options.push({text:"Safari",value:"safari"});
      scope.$apply();
      $("#dvDebug").text(JSON.stringify(scope.model.options));
    });
 
[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

Comments

# by tim

感謝這麼清楚的筆記跟分析. 沒想到這個部分有點過於複雜,不及 knockout/backbone 清楚。 不知道 angular 2.0 有沒有更清楚的做法?

# by Jeffrey

to tim, NG 2規格仍未底定,但可以確定變動偵測機制已大幅革新,變得更有效率,如果你跟我一樣懷念KO的Observable運作模式,好消息是NG2也提供新選擇:http://victorsavkin.com/post/110170125256/change-detection-in-angular-2

Post a comment