Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 938bd45

Browse files
sjbarkerNarretz
authored andcommittedMay 6, 2016
docs(guide/decorators): add decorator guide
+ explain decorators and how they are implemented in angular + explain how different types of services can be selected + explain `$delegate` objects and how they differ between services + warn of the risks/caveats of `$delegate` modification + note the exposure of `decorator` through the module api + show an example of decorating a core service + show an example of decorating a core directive + show an example of decorating a core filter Closes #12163 Closes #14372
1 parent 996b1e4 commit 938bd45

File tree

1 file changed

+486
-0
lines changed

1 file changed

+486
-0
lines changed
 

‎docs/content/guide/decorators.ngdoc

Lines changed: 486 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,486 @@
1+
@ngdoc overview
2+
@name Decorators
3+
@sortOrder 345
4+
@description
5+
6+
# Decorators in AngularJS
7+
8+
<div class="alert alert-warning">
9+
**NOTE:** This guide is targeted towards developers who are already familiar with AngularJS basics.
10+
If you're just getting started, we recommend the {@link tutorial/ tutorial} first.
11+
</div>
12+
13+
## What are decorators?
14+
15+
Decorators are a design pattern that is used to separate modification or *decoration* of a class without modifying the
16+
original source code. In Angular, decorators are functions that allow a service, directive or filter to be modified
17+
prior to its usage.
18+
19+
## How to use decorators
20+
21+
There are two ways to register decorators
22+
23+
- `$provide.decorator`, and
24+
- `module.decorator`
25+
26+
Each provide access to a `$delegate`, which is the instantiated service/directive/filter, prior to being passed to the
27+
service that required it.
28+
29+
### $provide.decorator
30+
31+
The {@link api/auto/service/$provide#decorator decorator function} allows access to a $delegate of the service once it
32+
has been instantiated. For example:
33+
34+
```js
35+
angular.module('myApp', [])
36+
37+
.config([ '$provide', function($provide) {
38+
39+
$provide.decorator('$log', [
40+
'$delegate',
41+
function $logDecorator($delegate) {
42+
43+
var originalWarn = $delegate.warn;
44+
$delegate.warn = function decoratedWarn(msg) {
45+
msg = 'Decorated Warn: ' + msg;
46+
originalWarn.apply($delegate, arguments);
47+
};
48+
49+
return $delegate;
50+
}
51+
]);
52+
}]);
53+
```
54+
55+
After the `$log` service has been instantiated the decorator is fired. The decorator function has a `$delegate` object
56+
injected to provide access to the service that matches the selector in the decorator. This `$delegate` will be the
57+
service you are decorating. The return value of the function *provided to the decorator* will take place of the service,
58+
directive, or filter being decorated.
59+
60+
<hr>
61+
62+
The `$delegate` may be either modified or completely replaced. Given a service `myService` with a method `someFn`, the
63+
following could all be viable solutions:
64+
65+
66+
#### Completely Replace the $delegate
67+
```js
68+
angular.module('myApp', [])
69+
70+
.config([ '$provide', function($provide) {
71+
72+
$provide.decorator('myService', [
73+
'$delegate',
74+
function myServiceDecorator($delegate) {
75+
76+
var myDecoratedService = {
77+
// new service object to replace myService
78+
};
79+
return myDecoratedService;
80+
}
81+
]);
82+
}]);
83+
```
84+
85+
#### Patch the $delegate
86+
```js
87+
angular.module('myApp', [])
88+
89+
.config([ '$provide', function($provide) {
90+
91+
$provide.decorator('myService', [
92+
'$delegate',
93+
function myServiceDecorator($delegate) {
94+
95+
var someFn = $delegate.someFn;
96+
97+
function aNewFn() {
98+
// new service function
99+
someFn.apply($delegate, arguments);
100+
}
101+
102+
$delegate.someFn = aNewFn;
103+
return $delegate;
104+
}
105+
]);
106+
}]);
107+
```
108+
109+
#### Augment the $delegate
110+
```js
111+
angular.module('myApp', [])
112+
113+
.config([ '$provide', function($provide) {
114+
115+
$provide.decorator('myService', [
116+
'$delegate',
117+
function myServiceDecorator($delegate) {
118+
119+
function helperFn() {
120+
// an additional fn to add to the service
121+
}
122+
123+
$delegate.aHelpfulAddition = helperFn;
124+
return $delegate;
125+
}
126+
]);
127+
}]);
128+
```
129+
130+
<div class="alert alert-info">
131+
Note that whatever is returned by the decorator function will replace that which is being decorated. For example, a
132+
missing return statement will wipe out the entire object being decorated.
133+
</div>
134+
135+
<hr>
136+
137+
Decorators have different rules for different services. This is because services are registered in different ways.
138+
Services are selected by name, however filters and directives are selected by appending `"Filter"` or `"Directive"` to
139+
the end of the name. The `$delegate` provided is dictated by the type of service.
140+
141+
| Service Type | Selector | $delegate |
142+
|--------------|-------------------------------|-----------------------------------------------------------------------|
143+
| Service | `serviceName` | The `object` or `function` returned by the service |
144+
| Directive | `directiveName + 'Directive'` | An `Array.<DirectiveObject>`<sub>{@link guide/decorators#drtvArray 1}</sub> |
145+
| Filter | `filterName + 'Filter'` | The `function` returned by the filter |
146+
147+
<small id="drtvArray">1. Multiple directives may be registered to the same selector/name</small>
148+
149+
<div class="alert alert-warning">
150+
**NOTE:** Developers should take care in how and why they are modifying the `$delegate` for the service. Not only
151+
should expectations for the consumer be kept, but some functionality (such as directive registration) does not take
152+
place after decoration, but during creation/registration of the original service. This means, for example, that
153+
an action such as pushing a directive object to a directive `$delegate` will likely result in unexpected behavior.
154+
155+
Furthermore, great care should be taken when decorating core services, directives, or filters as this may unexpectedly
156+
or adversely affect the functionality of the framework.
157+
</div>
158+
159+
### module.decorator
160+
161+
This {@link api/ng/type/angular.Module#decorator function} is the same as the `$provide.decorator` function except it is
162+
exposed through the module API. This allows you to separate your decorator patterns from your module config blocks. The
163+
main caveat here is that you will need to take note the order in which you create your decorators.
164+
165+
Unlike in the module config block (which allows configuration of services prior to their creation), the service must be
166+
registered prior to the decorator (see {@link guide/providers#provider-recipe Provider Recipe}). For example, the
167+
following would not work because you are attempting to decorate outside of the configuration phase and the service
168+
hasn't been created yet:
169+
170+
```js
171+
// will cause an error since 'someService' hasn't been registered
172+
angular.module('myApp').decorator('someService', ...);
173+
174+
angular.module('myApp').factory('someService', ...);
175+
```
176+
177+
## Example Applications
178+
179+
The following sections provide examples each of a service decorator, a directive decorator, and a filter decorator.
180+
181+
### Service Decorator Example
182+
183+
This example shows how we can replace the $log service with our own to display log messages.
184+
185+
<example module="myServiceDecorator" name="service-decorator">
186+
<file name="script.js">
187+
angular.module('myServiceDecorator', []).
188+
189+
controller('Ctrl', [
190+
'$scope',
191+
'$log',
192+
'$timeout',
193+
function($scope, $log, $timeout) {
194+
var types = ['error', 'warn', 'log', 'info' ,'debug'], i;
195+
196+
for (i = 0; i < types.length; i++) {
197+
$log[types[i]](types[i] + ': message ' + (i + 1));
198+
}
199+
200+
$timeout(function() {
201+
$log.info('info: message logged in timeout');
202+
});
203+
}
204+
]).
205+
206+
directive('myLog', [
207+
'$log',
208+
function($log) {
209+
return {
210+
restrict: 'E',
211+
template: '<ul id="myLog"><li ng-repeat="l in myLog" class="{{l.type}}">{{l.message}}</li></ul>',
212+
scope: {},
213+
compile: function() {
214+
return function(scope) {
215+
scope.myLog = $log.stack;
216+
};
217+
}
218+
};
219+
}
220+
]).
221+
222+
config([
223+
'$provide',
224+
function($provide) {
225+
226+
$provide.decorator('$log', [
227+
'$delegate',
228+
function logDecorator($delegate) {
229+
230+
var myLog = {
231+
warn: function(msg) {
232+
log(msg, 'warn');
233+
},
234+
error: function(msg) {
235+
log(msg, 'error');
236+
},
237+
info: function(msg) {
238+
log(msg, 'info');
239+
},
240+
debug: function(msg) {
241+
log(msg, 'debug');
242+
},
243+
log: function(msg) {
244+
log(msg, 'log');
245+
},
246+
stack: []
247+
};
248+
249+
function log(msg, type) {
250+
myLog.stack.push({ type: type, message: msg.toString() });
251+
if (console && console[type]) console[type](msg);
252+
}
253+
254+
return myLog;
255+
256+
}
257+
]);
258+
259+
}
260+
]);
261+
</file>
262+
263+
<file name="index.html">
264+
<div ng-controller="Ctrl">
265+
<h1>Logs</h1>
266+
<my-log></my-log>
267+
</div>
268+
</file>
269+
270+
<file name="style.css">
271+
li.warn { color: yellow; }
272+
li.error { color: red; }
273+
li.info { color: blue }
274+
li.log { color: black }
275+
li.debug { color: green }
276+
</file>
277+
278+
<file name="protractor.js" type="protractor">
279+
it('should display log messages in dom', function() {
280+
element.all(by.repeater('l in myLog')).count().then(function(count) {
281+
expect(count).toEqual(6);
282+
});
283+
});
284+
</file>
285+
</example>
286+
287+
### Directive Decorator Example
288+
289+
Failed interpolated expressions in `ng-href` attributes can easily go unnoticed. We can decorate `ngHref` to warn us of
290+
those conditions.
291+
292+
<example module="urlDecorator" name="directive-decorator">
293+
<file name="script.js">
294+
angular.module('urlDecorator', []).
295+
296+
controller('Ctrl', ['$scope', function ($scope) {
297+
$scope.id = 3;
298+
$scope.warnCount = 0; // for testing
299+
}]).
300+
301+
config(['$provide', function($provide) {
302+
303+
// matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions
304+
// from that markup (if they exist) and returns an array of those expressions
305+
function matchExpressions(str) {
306+
var exps = str.match(/{{([^}]+)}}/g);
307+
308+
// if there isn't any, get out of here
309+
if (exps === null) return;
310+
311+
exps = exps.map(function(exp) {
312+
var prop = exp.match(/[^{}]+/);
313+
return prop === null ? null : prop[0];
314+
});
315+
316+
return exps;
317+
}
318+
319+
// remember: directives must be selected by appending 'Directive' to the directive selector
320+
$provide.decorator('ngHrefDirective', [
321+
'$delegate',
322+
'$log',
323+
'$parse',
324+
function($delegate, $log, $parse) {
325+
326+
// store the original link fn
327+
var originalLinkFn = $delegate[0].link;
328+
329+
// replace the compile fn
330+
$delegate[0].compile = function(tElem, tAttr) {
331+
332+
// store the original exp in the directive attribute for our warning message
333+
var originalExp = tAttr.ngHref;
334+
335+
// get the interpolated expressions
336+
var exps = matchExpressions(originalExp);
337+
338+
// create and store the getters using $parse
339+
var getters = exps.map(function(el) {
340+
if (el) return $parse(el);
341+
});
342+
343+
return function newLinkFn(scope, elem, attr) {
344+
// fire the originalLinkFn
345+
originalLinkFn.apply($delegate[0], arguments);
346+
347+
// observe the directive attr and check the expressions
348+
attr.$observe('ngHref', function(val) {
349+
350+
// if we have getters and getters is an array...
351+
if (getters && angular.isArray(getters)) {
352+
353+
// loop through the getters and process them
354+
angular.forEach(getters, function(g, idx) {
355+
356+
// if val is truthy, then the warning won't log
357+
var val = angular.isFunction(g) ? g(scope) : true;
358+
if (!val) {
359+
$log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +
360+
'" is falsy!');
361+
362+
scope.warnCount++; // for testing
363+
}
364+
365+
});
366+
367+
}
368+
369+
});
370+
371+
};
372+
373+
};
374+
375+
// get rid of the old link function since we return a link function in compile
376+
delete $delegate[0].link;
377+
378+
// return the $delegate
379+
return $delegate;
380+
381+
}
382+
383+
]);
384+
385+
}]);
386+
</file>
387+
388+
<file name="index.html">
389+
<div ng-controller="Ctrl">
390+
<a ng-href="/products/{{ id }}/view" id="id3">View Product {{ id }}</a>
391+
- <strong>id == 3</strong>, so no warning<br>
392+
<a ng-href="/products/{{ id + 5 }}/view" id="id8">View Product {{ id + 5 }}</a>
393+
- <strong>id + 5 == 8</strong>, so no warning<br>
394+
<a ng-href="/products/{{ someOtherId }}/view" id="someOtherId">View Product {{ someOtherId }}</a>
395+
- <strong style="background-color: #ffff00;">someOtherId == undefined</strong>, so warn<br>
396+
<a ng-href="/products/{{ someOtherId + 5 }}/view" id="someOtherId5">View Product {{ someOtherId + 5 }}</a>
397+
- <strong>someOtherId + 5 == 5</strong>, so no warning<br>
398+
<div>Warn Count: {{ warnCount }}</div>
399+
</div>
400+
</file>
401+
402+
<file name="protractor.js" type="protractor">
403+
it('should warn when an expression in the interpolated value is falsy', function() {
404+
var id3 = element(by.id('id3'));
405+
var id8 = element(by.id('id8'));
406+
var someOther = element(by.id('someOtherId'));
407+
var someOther5 = element(by.id('someOtherId5'));
408+
409+
expect(id3.getText()).toEqual('View Product 3');
410+
expect(id3.getAttribute('href')).toContain('/products/3/view');
411+
412+
expect(id8.getText()).toEqual('View Product 8');
413+
expect(id8.getAttribute('href')).toContain('/products/8/view');
414+
415+
expect(someOther.getText()).toEqual('View Product');
416+
expect(someOther.getAttribute('href')).toContain('/products//view');
417+
418+
expect(someOther5.getText()).toEqual('View Product 5');
419+
expect(someOther5.getAttribute('href')).toContain('/products/5/view');
420+
421+
expect(element(by.binding('warnCount')).getText()).toEqual('Warn Count: 1');
422+
});
423+
</file>
424+
</example>
425+
426+
### Filter Decorator Example
427+
428+
Let's say we have created an app that uses the default format for many of our `Date` filters. Suddenly requirements have
429+
changed (that never happens) and we need all of our default dates to be `'shortDate'` instead of `'mediumDate'`.
430+
431+
<example module="filterDecorator" name="filter-decorator">
432+
<file name="script.js">
433+
angular.module('filterDecorator', []).
434+
435+
controller('Ctrl', ['$scope', function ($scope) {
436+
$scope.genesis = new Date(2010, 0, 5);
437+
$scope.ngConf = new Date(2016, 4, 4);
438+
}]).
439+
440+
config(['$provide', function($provide) {
441+
442+
$provide.decorator('dateFilter', [
443+
'$delegate',
444+
function dateDecorator($delegate) {
445+
446+
// store the original filter
447+
var originalFilter = $delegate;
448+
449+
// return our filter
450+
return shortDateDefault;
451+
452+
// shortDateDefault sets the format to shortDate if it is falsy
453+
function shortDateDefault(date, format, timezone) {
454+
if (!format) format = 'shortDate';
455+
456+
// return the result of the original filter
457+
return originalFilter(date, format, timezone);
458+
}
459+
460+
}
461+
462+
]);
463+
464+
}]);
465+
</file>
466+
467+
<file name="index.html">
468+
<div ng-controller="Ctrl">
469+
<div id="genesis">Initial Commit default to short date: {{ genesis | date }}</div>
470+
<div>ng-conf 2016 default short date: {{ ngConf | date }}</div>
471+
<div id="ngConf">ng-conf 2016 with full date format: {{ ngConf | date:'fullDate' }}</div>
472+
</div>
473+
</file>
474+
475+
<file name="protractor.js" type="protractor">
476+
it('should default date filter to short date format', function() {
477+
expect(element(by.id('genesis')).getText())
478+
.toMatch(/Initial Commit default to short date: \d{1,2}\/\d{1,2}\/\d{2}/);
479+
});
480+
481+
it('should still allow dates to be formatted', function() {
482+
expect(element(by.id('ngConf')).getText())
483+
.toMatch(/ng-conf 2016 with full date format\: [A-Za-z]+, [A-Za-z]+ \d{1,2}, \d{4}/);
484+
});
485+
</file>
486+
</example>

0 commit comments

Comments
 (0)
This repository has been archived.