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:
import { NgModule, Component } from '@angular/core';
@Component({
selector: 'example-component',
template: '<div>Woo a component!</div>',
})
export class ExampleComponent {
constructor() {
console.log('Hey I am a component!');
}
}
@NgModule({
imports: [],
declarations: [],
})
export class ExampleModule {
constructor() {
console.log('Hey I am a module!');
}
}
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.
import { Component, Input } from '@angular/core';
@Component({
selector: 'example-component',
template: '<div>Woo a component!</div>'
})
export class ExampleComponent {
@Input()
exampleProperty: string;
}
We would pass the input binding via a component property binding:
<example-component
[exampleProperty]="exampleData">
</example-component>
We had a different machine using a scope or bindToController with Directives, and bindings within the new component method:
const example component = {
bindings: {
exampleProperty: '<'',
},
template: `
<div>Woo a component!</div>
`,
controller: class ExampleComponent {
exampleProperty: string;
$onInit() {
// access this.exampleProperty
}
},
};
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.
For example:angular.module('myApp', [])
.config([ '$provide', function($provide) {
$provide.decorator('$log', [
'$delegate',
function $logDecorator($delegate) {
var originalWarn = $delegate.warn;
$delegate.warn = function decoratedWarn(msg) {
msg = 'Decorated Warn: ' + msg;
originalWarn.apply($delegate, arguments);
};
return $delegate;
}
]);
}]);
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
angular.module('myApp', [])
.config([ '$provide', function($provide) {
$provide.decorator('myService', [
'$delegate',
function myServiceDecorator($delegate) {
var myDecoratedService = {
// new service object to replace myService
};
return myDecoratedService;
}
]);
}]);
Patch the $delegate
angular.module('myApp', [])
.config([ '$provide', function($provide) {
$provide.decorator('myService', [
'$delegate',
function myServiceDecorator($delegate) {
var someFn = $delegate.someFn;
function aNewFn() {
// new service function
someFn.apply($delegate, arguments);
}
$delegate.someFn = aNewFn;
return $delegate;
}
]);
}]);
Augment the $delegate
angular.module('myApp', [])
.config([ '$provide', function($provide) {
$provide.decorator('myService', [
'$delegate',
function myServiceDecorator($delegate) {
function helperFn() {
// an additional fn to add to the service
}
$delegate.aHelpfulAddition = helperFn;
return $delegate;
}
]);
}]);
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 Type | Selector | $delegate |
---|---|---|
Service | serviceName | The objector function returned by the service. |
Directive | directiveName + ‘Directive’ | An Array 1 |
Filter | filterName + ‘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:
angular.module('theApp', [])
.factory('theFactory', theFactoryFn)
.config(function($provide) {
$provide.decorator('theFactory', provideDecoratorFn); // runs first
})
.decorator('theFactory', moduleDecoratorFn); // runs seconds
the service has been declared multiple times, a decorator will decorate the service that has been declared last:-
angular
.module('theApp', [])
.factory('theFactory', theFactoryFn)
.decorator('theFactory', moduleDecoratorFn)
.factory('theFactory', theOtherFactoryFn);
// `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
Angular.module('myServiceDecorator', []).
controller('Ctrl', [
'$scope',
'$log',
'$timeout',
function($scope, $log, $timeout) {
var types = ['error', 'warn', 'log', 'info' ,'debug'], i;
for (i = 0; i < types.length; i++) {
$log[types[i]](types[i] + ': message ' + (i + 1));
}
$timeout(function() {
$log.info('info: message logged in timeout');
});
}
]).
directive('myLog', [
'$log',
function($log) {
return {
restrict: 'E',
template: '<ul id="myLog"><li ng-repeat="l in myLog" class="{{l.type}}">{{l.message}}</li></ul>',
scope: {},
compile: function() {
return function(scope) {
scope.myLog = $log.stack;
};
}
};
}
]).
config([
'$provide',
function($provide) {
$provide.decorator('$log', [
'$delegate',
function logDecorator($delegate) {
var myLog = {
warn: function(msg) {
log(msg, 'warn');
},
error: function(msg) {
log(msg, 'error');
},
info: function(msg) {
log(msg, 'info');
},
debug: function(msg) {
log(msg, 'debug');
},
log: function(msg) {
log(msg, 'log');
},
stack: []
};
function log(msg, type) {
myLog.stack.push({ type: type, message: msg.toString() });
if (console && console[type]) console[type](msg);
}
return myLog;
}
]);
}
]);
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
Angular.module('urlDecorator', []).
controller('Ctrl', ['$scope', function($scope) {
$scope.id = 3;
$scope.warnCount = 0; // for testing
}]).
config(['$provide', function($provide) {
// matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions
// from that markup (if they exist) and returns an array of those expressions
function matchExpressions(str) {
var exps = str.match(/{{([^}]+)}}/g);
// if there isn't any, get out of here
if (exps === null) return;
exps = exps.map(function(exp) {
var prop = exp.match(/[^{}]+/);
return prop === null ? null : prop[0];
});
return experts;
}
// remember: directives must be selected by appending 'Directive' to the directive selector
$provide.decorator('ngHrefDirective', [
'$delegate',
'$log',
'$parse',
function($delegate, $log, $parse) {
// store the original link fn
var originalLinkFn = $delegate[0].link;
// replace the compile fn
$delegate[0].compile = function(tElem, tAttr) {
// store the original exp in the directive attribute for our warning message
var originalExp = tAttr.ngHref;
// get the interpolated expressions
var exps = matchExpressions(originalExp);
// create and store the getters using $parse
var getters = exps.map(function(exp) {
return exp && $parse(exp);
});
return function newLinkFn(scope, elem, attr) {
// fire the originalLinkFn
originalLinkFn.apply($delegate[0], arguments);
// observe the directive attr and check the expressions
attr.$observe('ngHref', function(val) {
// if we have getters and getters is an array...
if (getters && angular.isArray(getters)) {
// loop through the getters and process them
angular.forEach(getters, function(g, idx) {
// if val is truthy, then the warning won't log
var val = angular.isFunction(g) ? g(scope) : true;
if (!val) {
$log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +
'" is falsy!');
scope.warnCount++; // for testing
}
});
}
});
};
};
// get rid of the old link function since we return a link function in compile
delete $delegate[0].link;
// return the $delegate
return $delegate;
}
]);
}]);
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
Angular.module('filterDecorator', []).
controller('Ctrl', ['$scope', function($scope) {
$scope.genesis = new Date(2010, 0, 5);
$scope.ngConf = new Date(2016, 4, 4);
}]).
config(['$provide', function($provide) {
$provide.decorator('dateFilter', [
'$delegate',
function dateDecorator($delegate) {
// store the original filter
var originalFilter = $delegate;
// return our filter
return shortDateDefault;
// shortDateDefault sets the format to shortDate if it is falsy
function shortDateDefault(date, format, timezone) {
if (!format) format = 'shortDate';
// return the result of the original filter
return originalFilter(date, format, timezone);
}
}
]);
}]);
Leave a Reply