AngularJS Decorating Directives

為什麼需要用到 Decorators?

其一是你不想破壞原有的行為或是原有的程式碼,

去擴增或是覆寫現有功能,

之前一篇 JSNLog integrate to AngularJS 就是利用了 Decorator 裝飾掉 expectionHandle,

今天這篇主來更深入研究 AngularJS Decorator 

首先我們來關注官方的說明文件,

與 Service 最大不同於在它的回傳型態是 Array<DirectiveObject> 而不是我們之前看到的 Array 或 Object,

為什麼 Angular 會回傳 Array ? 這邊它的解釋寫道會有複數以上註冊相同的選擇器/名字.

不過在實務上這件事理應也是不該發生.

註冊相同的名字除了混淆代碼之外想不到有什麼好處.

讓我們先來看看個簡單範例:

HTML

  <div ng-app="app" ng-controller="app as self">
    <p ng-bind="self.name"></p>
    <menu></menu>
  </div>

 JS

angular
  .module('app', [])
  .controller('app', [function(){
    this.name = 'Angular 控管...';
  }])
  .directive('menu', [function(){
    return {
      template: '<ul><li>1</li><li>2</li><li>3</li></ul>',
      restrict: 'A',
      replace: true
    };
  }])
  .config(['$provide', function($provide){
    $provide.decorator('menuDirective', function($delegate){
      var directive = $delegate[0];
      directive.restrict = "E";
      
      return $delegate;
    });
  }]);

Try it - JS Bin on jsbin.com

由範例中可以清楚的看到 restrict 從原本的 'A' 改為 'E' 了,

當然你也可以擴展成 'AE',

接下來看個使用 link 函式的 directive 要如何去擴展.

HTML

  <div ng-app="app" ng-controller="app as self">
    <p ng-bind="self.name"></p>
    <my-label name="Press this section"></my-label>
  </div>

JS

angular
  .module('app', [])
  .controller('app', [function(){
    var self = this;
    self.name = 'Angular 控管...';
  }])
  .directive('myLabel', [function(){
    return {
      template: '<p style="background-color: yellow;">{{name}}</p>',
      restrict: 'E',
      replace: true,
      scope: { name: '@' },
      link: function(scope, ele, attrs){
        if(angular.isDefined(attrs.name)){
          attrs.name = attrs.name + " !";
        }
      }
    };
  }])

我們想要加入個觸發 click 事件又不要破壞掉原本 link 函式的行為,

一樣使用 decorator 去裝飾掉, 如果熟知 directive 生命週期的人很快就能聯想到 compile.

我們可以利用 compile 的 element 元素去綁定 click 事件, 再利用 $apply 去通知 angular 去觸發 $digest,

  .config(['$provide', function($provide){
    $provide.decorator('myLabelDirective', function($delegate){
      var directive = $delegate[0];
      directive.scope.fn = "&";
      var link = directive.link;
      
      directive.compile = function() {
        return function(scope, ele, attrs){
          link.apply(this, arguments);
          
          ele.bind('click', function(){
            scope.$apply(function(){
              scope.fn();
            });
          });
        };
      };
      
      return $delegate;
    });
  }]);

Try it - JS Bin on jsbin.com

接下來再講一個工作上的實務應用,

由於工作上撰寫經常被使用的 directive 需要被用在另外的頁面上,

但是在另外的頁面上動作跟行為跟原本的 directive 會有一些不同,

到底是我要把整份代碼以及依賴的代碼搬過去 ? 還是重新再寫一個專門為了那個特殊頁面客製化的 directive?

於是我就使用了 decorator 去裝飾掉原本的也很完美的去達到另外的特殊需求!

HTML

  <div ng-app="app" ng-controller="app as self">
    <p ng-bind="self.application"></p>
    <div class="row">
      <techprd ng-repeat="item in self.Models" m="item"></techprd>
    </div>
  </div>

JS

angular
  .module('app', [])
  .controller('app', [function(){
    var self = this;
    self.application = 'Decorator Directive Controller';
    self.Models = [];
    
    for(var i = 0; i < 6; i++) {
      var price = Math.floor((Math.random() * 1000) + 1);
      self.Models.push(price);
    }
  }])
  .directive('techprd', [function(){
    ctrl.$inject = ['$window'];
    function ctrl($window){
      var self = this;
      self.image = "https://pixabay.com/static/uploads/photo/2016/06/29/17/14/water-1487304_960_720.jpg";
      self.go = function(){
        alert(self.m);
      };
    }
    
    return {
      template: '<div class="col-xs-2" ng-click="self.go()"><img ng-src="{{self.image}}" /><p><label>NT$ </label><span ng-bind="self.m"></span></p></div>',
      restrict: 'E',
      replace: true,
      scope: { m: '<' },
      controller: ctrl,
      controllerAs: 'self',
      bindToController: true
    };
  }])

OK, 上述代碼的顯示結果應該會如下.

點選其中個圖片則會 alert 出價錢.

現在有兩個需求分別是

  1. 每個圖片上面多顯示一段文字 : Free Shipping
  2. 點選圖片會再 Console 印出價錢

代碼如下,

  .config(['$provide', function($provide){
    dectrl.$inject = ['$delegate', '$controller'];
    
    function dectrl($delegate, $controller){
      var directive = $delegate[0];
      var original = angular.copy(directive.controller);
      var arr = directive.template.split('<img ng-src');
      directive.template = arr[0] + '<p><span class="label label-primary">Free Shipping</span></p>' + '<img ng-src' + arr[1];
      
      directive.controller = function($window){
        var controller = $controller(original, { $window: $window });
        
        var _go = angular.copy(controller.go);
        controller.go = function(){
          console.log(this.m);
          _go.call(this);
        };
        
        return controller;
      };
      
      return $delegate;
    }
    
    $provide.decorator('techprdDirective', dectrl);
  }]);

Try it - JS Bin on jsbin.com

可以看到使用 $controller service 去生成實例後再去擴增行為,

所以再不改源代碼的前提下我們完成擴增動作,

越深入研究這 AngularJS 這套框架越覺得偉大厲害,

能夠把很多設計切得很漂亮,

文章參考:

Experiment: Decorating Directives