Tuesday 28 December 2021

What is Angular Decorators?

 Decorators are design patterns used to isolate the modification or decoration of a class without modifying the source code.

In AngularJS, decorators are functions that allow a service, directive, or filter to be modified before it is used.

There are four main types of angular decorators:

  • Class decorators, such as @Component and @NgModule
  • Property decorators for properties inside classes, such as @Input and @Output
  • Method decorators for methods inside classes, such as @HostListener
  • Parameter decorators for parameters inside class constructors, such as @Inject

Each decorator has a unique role. Let's see some examples to expand the types on the list above.

Classroom decorator

Angular provides us with some class decorators. They allow us to tell Angular a particular class is a component or a module, e.g., And the decorator allows us to define this effect without putting any code inside the class.

An @Component and @NgModel decorator used with classes:

  1. import { NgModule, Component } from '@angular/core';  
  2. @Component({  
  3.   selector: 'example-component',  
  4.   template: '<div>Woo a component!</div>',  
  5. })  
  6. export class ExampleComponent {  
  7.   constructor() {  
  8.     console.log('Hey I am a component!');  
  9.   }  
  10. }  
  11. @NgModule({  
  12.   imports: [],  
  13.   declarations: [],  
  14. })  
  15. export class ExampleModule {  
  16.   constructor() {  
  17.     console.log('Hey I am a module!');  
  18.   }  
  19. }  

It is a component or a module where no code is needed in the class to tell Angular. We need to decorate it, and Angular will do the rest.

Property Decorator

These are the second most common decorators you'll see. They allow us to decorate some properties within our classes.

Imagine we have a property in our class that we want to be an InputBinding.

We have to define this property in our class for TypeScript without the decorator, and tell Angular that we have a property we want to be an input.

With the decorator, we can simply place the @Input() decorator above the property - which AngularJS compiler will create an input binding with the property name and link them.

  1. import { Component, Input } from '@angular/core';  
  2. @Component({  
  3.   selector: 'example-component',  
  4.   template: '<div>Woo a component!</div>'  
  5. })  
  6. export class ExampleComponent {  
  7.   @Input()  
  8.   exampleProperty: string;  
  9. }  

We would pass the input binding via a component property binding:

  1. <example-component  
  2.   [exampleProperty]="exampleData">  
  3. </example-component>  

We had a different machine using a scope or bindToController with Directives, and bindings within the new component method:

  1. const example component = {  
  2.   bindings: {  
  3.     exampleProperty: '<'',  
  4.   },  
  5.   template: `  
  6.     <div>Woo a component!</div>  
  7.   `,  
  8.   controller: class ExampleComponent {  
  9.     exampleProperty: string;  
  10.     $onInit() {  
  11.       // access this.exampleProperty  
  12.     }  
  13.   },  
  14. };  
  15. angular.module('app').component('exampleComponent', example component);  

You see above that we have two different properties to maintain. However, a single property instance property is decorated, which is easy to change, maintain and track as our codebase grows.

How to use a decorator?

There are two ways to register decorators

  • Provide $. decorator and
  • Modulus. decorator

Each provides access to the $delegate, which is an immediate service/directive/filter, before being passed to the service that needs it.

$provide.decorator

The decorator function allows access to the $delegate of the service once when it is instantiated.

  1. For example:angular.module('myApp', [])  
  2. .config([ '$provide', function($provide) {  
  3.   
  4.   $provide.decorator('$log', [  
  5.     '$delegate',  
  6.     function $logDecorator($delegate) {  
  7.   
  8.       var originalWarn = $delegate.warn;  
  9.       $delegate.warn = function decoratedWarn(msg) {  
  10.         msg = 'Decorated Warn: ' + msg;  
  11.         originalWarn.apply($delegate, arguments);  
  12.       };  
  13.  return $delegate;  
  14.     }  
  15.   ]);  
  16. }]);  

After the $log service is instantiated, the decorator is fired. In the decorator function, a $delegate object is injected to provide access to the service matching the selector in the decorator. The $delegate will be the service you are decorating.

The function's return value passed to the decorator is the service, directive, or filter being decorated.

$delegate can be either modified or replaced entirely.

Completely Replace the $delegate

  1. angular.module('myApp', [])  
  2. .config([ '$provide', function($provide) {  
  3.   $provide.decorator('myService', [  
  4.     '$delegate',  
  5.     function myServiceDecorator($delegate) {  
  6.       var myDecoratedService = {  
  7.         // new service object to replace myService  
  8.       };  
  9.       return myDecoratedService;  
  10.     }  
  11.   ]);  
  12. }]);  
  13. Patch the $delegate  
  14. angular.module('myApp', [])  
  15. .config([ '$provide', function($provide) {  
  16.   $provide.decorator('myService', [  
  17.     '$delegate',  
  18.     function myServiceDecorator($delegate) {  
  19.       var someFn = $delegate.someFn;  
  20.       function aNewFn() {  
  21.         // new service function  
  22.         someFn.apply($delegate, arguments);  
  23.       }  
  24.       $delegate.someFn = aNewFn;  
  25.       return $delegate;  
  26.     }  
  27.   ]);  
  28. }]);  
  29. Augment the $delegate  
  30. angular.module('myApp', [])  
  31. .config([ '$provide', function($provide) {  
  32.   $provide.decorator('myService', [  
  33.     '$delegate',  
  34.     function myServiceDecorator($delegate) {  
  35.       function helperFn() {  
  36.         // an additional fn to add to the service  
  37.       }  
  38.       $delegate.aHelpfulAddition = helperFn;  
  39.       return $delegate;  
  40.     }  
  41.   ]);  
  42. }]);  

Whatever is returned by the decorator function will replace that which is being decorated.

For example, a missing return statement will wipe out the entire object being decorated.

Decorators have different rules for different services. It is because services are registered in different ways. Services are selected by name appending "Filter" or "Directive" selected to the name's end. The type of service dictates the $delegate provided.

Service TypeSelector$delegate
ServiceserviceNameThe objector function returned by the service.
DirectivedirectiveName + 'Directive'An Array 1
FilterfilterName + 'Filter'The function returned by the filter

1. Multiple directives will registered to the same selector

Developers should be mindful of how and why they modify $delegate for the service. Not only must there be expectations for the consumer, but some functionality does not occur after decoration but during the creation/registration of the native service.

For example, pushing an instruction such as an instruction object to an instruction $delegate can lead to unexpected behavior.

In addition, great care must be taken when decorating core services, directives, or filters, as this can unexpectedly or adversely affect the framework's functionality.

Modulus. decorator

This function is the same as $provide. It is exposed through the module API except for the decorator function. It allows you to separate your decorator pattern from your module config block.

The decorator function runs during the configuration phase of the app with $provided. Decorator module. The decorator is defined before the decorated service.

You can implement multiple decorators, it is worth mentioning that the decorator application always follows the order of declaration:

If a service is decorated by both $provide.decorator and module.decorator , the decorators are applied in the order:

  1. angular.module('theApp', [])  
  2. .factory('theFactory', theFactoryFn)  
  3. .config(function($provide) {  
  4.   $provide.decorator('theFactory', provideDecoratorFn); // runs first  
  5. })  
  6. .decorator('theFactory', moduleDecoratorFn); // runs seconds  

the service has been declared multiple times, a decorator will decorate the service that has been declared last:-

  1. angular  
  2.   .module('theApp', [])  
  3.   .factory('theFactory', theFactoryFn)  
  4.   .decorator('theFactory', moduleDecoratorFn)  
  5.   .factory('theFactory', theOtherFactoryFn);  
  6.   
  7. // `theOtherFactoryFn` is selected as 'theFactory' provider `.  

Example Applications

The below sections provide examples of each service decorator, a directive decorator, and a filter decorator.

Service Decorator Example

This example shows how we can replace the $log service with our own to display log messages.

script.jsindex.htmlstyle.cssprotractor.js

  1. Angular.module('myServiceDecorator', []).  
  2. controller('Ctrl', [  
  3.   '$scope',  
  4.   '$log',  
  5.   '$timeout',  
  6.   function($scope, $log, $timeout) {  
  7.     var types = ['error''warn''log''info' ,'debug'], i;  
  8.     for (i = 0; i < types.length; i++) {  
  9.       $log[types[i]](types[i] + ': message ' + (i + 1));  
  10.     }  
  11.     $timeout(function() {  
  12.       $log.info('info: message logged in timeout');  
  13.     });  
  14.   }  
  15. ]).  
  16. directive('myLog', [  
  17.   '$log',  
  18.   function($log) {  
  19.     return {  
  20.       restrict: 'E',  
  21.       template: '<ul id="myLog"><li ng-repeat="l in myLog" class="{{l.type}}">{{l.message}}</li></ul>',  
  22.       scope: {},  
  23.       compile: function() {  
  24.         return function(scope) {  
  25.           scope.myLog = $log.stack;  
  26.         };  
  27.       }  
  28.     };  
  29.   }  
  30. ]).  
  31. config([  
  32.   '$provide',  
  33.   function($provide) {  
  34.     $provide.decorator('$log', [  
  35.       '$delegate',  
  36.       function logDecorator($delegate) {  
  37.         var myLog = {  
  38.           warn: function(msg) {  
  39.             log(msg, 'warn');  
  40.           },  
  41.           error: function(msg) {  
  42.             log(msg, 'error');  
  43.           },  
  44.           info: function(msg) {  
  45.             log(msg, 'info');  
  46.           },  
  47.           debug: function(msg) {  
  48.             log(msg, 'debug');  
  49.           },  
  50.           log: function(msg) {  
  51.             log(msg, 'log');  
  52.           },  
  53.           stack: []  
  54.         };  
  55.         function log(msg, type) {  
  56.           myLog.stack.push({ type: type, message: msg.toString() });  
  57.           if (console && console[type]) console[type](msg);  
  58.         }  
  59.         return myLog;  
  60.       }  
  61.     ]);  
  62.   }  
  63. ]);  

Directive Decorator Example

The Failed interpolated expressions in ng-href attributes will easily unnoticed. We can decorate ngHref to warn us of those conditions.

script.jsindex.htmlprotractor.js

  1. Angular.module('urlDecorator', []).  
  2. controller('Ctrl', ['$scope', function($scope) {  
  3.   $scope.id = 3;  
  4.   $scope.warnCount = 0// for testing  
  5. }]).  
  6. config(['$provide', function($provide) {  
  7.   // matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions  
  8.   // from that markup (if they exist) and returns an array of those expressions  
  9.   function matchExpressions(str) {  
  10.     var exps = str.match(/{{([^}]+)}}/g);  
  11.     // if there isn't any, get out of here  
  12.     if (exps === nullreturn;  
  13.     exps = exps.map(function(exp) {  
  14.       var prop = exp.match(/[^{}]+/);  
  15.       return prop === null ? null : prop[0];  
  16.     });  
  17.     return experts;  
  18.   }  
  19.   // remember: directives must be selected by appending 'Directive' to the directive selector  
  20.   $provide.decorator('ngHrefDirective', [  
  21.     '$delegate',  
  22.     '$log',  
  23.     '$parse',  
  24.     function($delegate, $log, $parse) {  
  25.       // store the original link fn  
  26.       var originalLinkFn = $delegate[0].link;  
  27.       // replace the compile fn  
  28.       $delegate[0].compile = function(tElem, tAttr) {  
  29.         // store the original exp in the directive attribute for our warning message  
  30.         var originalExp = tAttr.ngHref;  
  31.         // get the interpolated expressions  
  32.         var exps = matchExpressions(originalExp);  
  33.         // create and store the getters using $parse  
  34.         var getters = exps.map(function(exp) {  
  35.           return exp && $parse(exp);  
  36.         });  
  37.         return function newLinkFn(scope, elem, attr) {  
  38.           // fire the originalLinkFn  
  39.           originalLinkFn.apply($delegate[0], arguments);  
  40.           // observe the directive attr and check the expressions  
  41.           attr.$observe('ngHref', function(val) {  
  42.             // if we have getters and getters is an array...  
  43.             if (getters && angular.isArray(getters)) {  
  44.               // loop through the getters and process them  
  45.               angular.forEach(getters, function(g, idx) {  
  46.                 // if val is truthy, then the warning won't log  
  47.                 var val = angular.isFunction(g) ? g(scope) : true;  
  48.                 if (!val) {  
  49.                   $log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +  
  50.                     '" is falsy!');  
  51.                   scope.warnCount++; // for testing  
  52.                 }  
  53.               });  
  54.             }  
  55.           });  
  56.         };  
  57.       };  
  58.       // get rid of the old link function since we return a link function in compile  
  59.       delete $delegate[0].link;  
  60.       // return the $delegate  
  61.       return $delegate;  
  62.     }  
  63.   ]);  
  64. }]);  

Filter Decorator Example

We have created an application that uses the default format for many of our Date filters. We need all of our default dates to be 'shortDate' instead of 'mediumDate'.

script.jsindex.htmlprotractor.js

  1. Angular.module('filterDecorator', []).  
  2. controller('Ctrl', ['$scope', function($scope) {  
  3.   $scope.genesis = new Date(201005);  
  4.   $scope.ngConf = new Date(201644);  
  5. }]).  
  6. config(['$provide', function($provide) {  
  7.   $provide.decorator('dateFilter', [  
  8.     '$delegate',  
  9.     function dateDecorator($delegate) {  
  10.       // store the original filter  
  11.       var originalFilter = $delegate;  
  12.       // return our filter  
  13.       return shortDateDefault;  
  14.       // shortDateDefault sets the format to shortDate if it is falsy  
  15.       function shortDateDefault(date, format, timezone) {  
  16.         if (!format) format = 'shortDate';  
  17.         // return the result of the original filter  
  18.         return originalFilter(date, format, timezone);  
  19.       }  
  20.     }  
  21.   ]);  
  22. }]);  

Decorators are a core concept when developed with Angular. The internal codebase makes extensive use of decorators.

No comments:

Post a Comment