NG筆記8-初試自訂Directive
2 | 11,373 |
實做對象: KO範例6 - 陣列元素的新增/移除事件。
<!DOCTYPE html>
<html ng-app="sampleApp">
<head>
<meta charset="utf-8">
<title>KO範例6 - 陣列元素的新增/移除事件</title>
<style>
table { width: 400px }
td,th { border: 1px solid gray; text-align: center }
a.btn { text-decoration: underline; cursor: pointer; color: blue; }
tr.new { color: brown; }
</style>
</head>
<body ng-controller="defaultCtrl">
<input type="button" value="新增User" ng-click="model.addUser()" />
共 <span>{{ model.users.length }}</span> 筆,
合計 <span>{{ model.calcTotalScore() | number:0 }}</span> 分
<table>
<thead>
<tr>
<th>Id</th>
<th>姓名</th>
<th>積分</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in model.users" ng-class="user.addFlag ? 'new' : ''"
anim-hide="user.removeFlag" anim-hide-done="model.removeUser()">
<td><span>{{ user.id }}</span>
</td>
<td><span>{{ user.name }}</span>
</td>
<td><span style='text-align: right'>{{ user.score }}</span>
</td>
<td><a ng-click="model.markUserRemoved(user)" class="btn">移除</a>
</td>
</tr>
</tbody>
</table>
<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>
angular.module("sampleApp", [])
.directive("animHide", function() {
return function(scope, element, attrs) {
scope.$watch(attrs["animHide"], function(newValue, oldValue) {
if (newValue == true) {
element.css("background-color", "red")
.animate({ opacity: 0.2 }, 500, function () {
scope.$parent.$apply(attrs["animHideDone"]);
})
}
});
};
})
.controller("defaultCtrl", function($scope) {
//很簡單的User資料物件
function UserViewModel(id, name, score) {
var self = this;
self.id = id;
self.name = name;
self.score = score;
self.removeFlag = false;
self.addFlag = false;
}
function myViewModel() {
var self = this;
self.users = [];
self.userToRemove = null;
self.markUserRemoved = function(user) {
user.removeFlag = true;
self.userToRemove = user;
};
self.removeUser = function() {
self.users.splice(self.users.indexOf(self.userToRemove), 1);
}
var c = 3;
self.addUser = function() {
var now = new Date(); //用時間產生隨機屬性值
var user = new UserViewModel(
"M" + c++,
"P" + "-" + now.getSeconds() * now.getMilliseconds(),
now.getMilliseconds());
//將現有user.addFlag清掉
$.each(self.users, function(i, u) { u.addFlag = false; });
user.addFlag = true;
self.users.push(user);
};
self.calcTotalScore = function() {
var sum = 0;
$.each(self.users, function(i, user) {
sum += user.score;
});
return sum;
};
}
vm = new myViewModel();
vm.users.push(
new UserViewModel("M1", "Jeffrey", 32767));
vm.users.push(
new UserViewModel("M2", "Darkthread", 65535));
$scope.model = vm;
});
</script>
</body>
</html>
NG沒有ko.observableArray()物件,不像有KO有afterAdd/beforeRemove事件可用,為了實現跟KO範例6一樣的效果,使用以下技巧:
- 新增資料時,將最新加入的資料標為暗紅字
ViewModel增加addFlag屬性,新增User ng-click事件將現存資料的addFlag全設成false,只留新資料為true。再透過ng-class="user.addFlag ? 'new' : ''"讓addFlag==true的<tr>套用暗紅樣式。 - 刪除資料時,加上資料淡出後消失的特效
由於要在淡出動畫結束後再刪除資料,我想到最直覺的做法是透過jQuery.animate()遞減opacity並在動畫結束事件刪除資料,但ViewModel不該涉及UI,決定寫下第一個自訂Directive當練習。
透過module().directive()可以加入自訂的Directive,我們設計一個animHide Directive,藉由偵測指定屬性變化,在屬性值變成true時觸發jQuery.hide()動畫效果,並在動畫結束後呼叫指定函式(執行刪除動作)。把這段程式抽出來單獨看:
<script>
angular.module("sampleApp", [])
.directive("animHide", function() {
return function(scope, element, attrs) {
scope.$watch(attrs["animHide"], function(newValue, oldValue) {
if (newValue == true) {
element.css("background-color", "red")
.animate({ opacity: 0.2 }, 500, function () {
scope.$parent.$apply(attrs["animHideDone"]);
})
}
});
};
})
.controller("defaultCtrl", function($scope) {
//...略...
});
</script>
.directive("animHide", function(){ ... })的第二個函式參數會在建立Directive時呼叫,需傳回Directive設定物件,針對較簡單的Directive,只需指定設定物件中的link函式屬性。原本應該傳回return { scope: …, restrict: …, link: function() { … }, … },寫成return funtion(scope, element, attrs) { ... },NG就視為只指定link函式,其他設定使用預設值。
link函式會在Directive套用到UI元素時執行(可以想成初始化期間要完成的工作),在函式中可透過scope存取當下的ViewModel物件,用element存取UI元素的jQuery物件,attrs則讓Directive經由HTML Attribute讀取額外參數。在這個案例中,我們不把動畫結束後的事寫死在程式裡,而是用animHideDone="刪除動作"指定(注意: 在HTML寫成anim-hide-done="",讀取時寫成Camel格式: attrs["animHideDone"]),這樣比較彈性。補充一點: 在Angular的設計中,ViewModel不應涉及UI,以貫徹SoC棈神並利於單元測試,Directive是放置ViewModel與HTML元素互動邏輯的最佳場合,
在link函式裡,我們第一件要做的事是要求NG注意某個屬性值的變化,一旦改變時要通知我們。scope.$watch()是NG用來建立連動關係的重要方法,類似KO的ko.computed(),第一個參數傳入表示式字串或函式,NG評估表示式字串或函式傳回結果,一旦其關聯屬性出現變化,便會觸發第二個參數指定的事件,事件可取得關聯屬性的新、舊值(newValue及oldValue),依新舊值決定如何因應。在我們的Diretive中,一旦newValue為true時,透過jQuery先將element背景改為紅色,再執行顏色刷淡(opacity由1降為0.2)動畫效果,結束後執行animHideDone所指定的刪除動作,為了確保NG感測到刪除元素的變化,記得要用scope.$apply()執行。
定義好animHide Directive,我們在<tr>加上宣告: <tr ng-repeat="user in model.users" ng-class="user.addFlag ? 'new' : ''" anim-hide="user.removeFlag" anim-hide-done="model.removeUser()">,背後會$watch("user.removeFlag", …),一旦user.removeFlag為true,就引發背景色變紅並淡出,動畫結束時執行model.removeUser()將User從陣列移除。
如此,要刪除資料時,將removeFlag設為true,就會先播動畫再移除資料,符合規格要求。
Comments
# by metavige
https://docs.angularjs.org/api/ng/directive/ngSwitch 可以嘗試用用看 ngSwitch
# by Jeffrey
to metavige, 謝謝分享。