復刻對象: KO範例3 - 動態新增下拉選單選項。
先示範一個失敗寫法。在KO範例裡,新增選項按鈕不包含在ViewModel範圍內,而是透過jQuery click事件在選項集合新增物件,而選項集合是ko.observabelArray(),KO能感測到新增動作,同步增加下拉選單選項;但同樣做法直接搬到NG行不通,option是尋常JavaScript陣列,NG感測不到Scope之外對ViewModel屬性的更動。如以下程式,按鈕後vm.options陣列雖已加入新元素,卻不會反應到下拉選單。Live Demo
<!DOCTYPEhtml>
<htmlng-app="sampleApp">
<head>
<metacharset="utf-8">
<title>Lab 3 - 動態增加SELECT選項(無效)</title>
</head>
<bodyng-controller="defaultCtrl">
<selectid="selOptions"style="width: 120px"
ng-options="item.text for item in model.options"ng-model="model.result">
</select>
Result=<spanng-bind="model.result.value"></span>
<divstyle="margin-top: 10px">
Text: <inputid='txtOptText'value="Firefox"/>
Value: <inputid='txtOptValue'value="ff"/>
<inputtype="button"value="新增選項"id='btnAddOpt'/>
<divid="dvDebug"></div>
</div>
<scriptsrc="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
<inputtype="button"value="新增選項"id='btnAddOpt'ng-click="model.addOption()"/>
</div>
<scriptsrc="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));
});