實做對象: KO範例6 - 陣列元素的新增/移除事件

Live Demo

<!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,就會先播動畫再移除資料,符合規格要求。

[NG系列]
http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

Comments

# by metavige

https://docs.angularjs.org/api/ng/directive/ngSwitch 可以嘗試用用看 ngSwitch

# by Jeffrey

to metavige, 謝謝分享。

Post a comment