Skip to content

Commit 31044cc

Browse files
committed
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 angular#12163
1 parent 4b2bc60 commit 31044cc

File tree

1 file changed

+368
-0
lines changed

1 file changed

+368
-0
lines changed

docs/content/guide/decorators.ngdoc

+368
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
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 used to facilitate a decorator design pattern. This pattern is used to separate modification or
16+
*decoration* of a class without modifying the original source code. In Angular, decorators are functions that allow a
17+
service to be modified after their instantiation.
18+
19+
## $provide.decorator
20+
21+
The {@link api/auto/service/$provide#decorator decorator function} allows access to a $delegate of the service once it
22+
has been instantiated. For example:
23+
24+
```js
25+
angular.module('myApp', [])
26+
27+
.config([ '$provide', function($provide) {
28+
29+
$provide.decorator('$log', [
30+
'$delegate',
31+
function $logDecorator($delegate) {
32+
33+
var originalWarn = $delegate.warn;
34+
$delegate.warn = function decoratedWarn(msg) {
35+
msg = 'Decorated Warn: ' + msg;
36+
originalWarn.apply($delegate, arguments);
37+
};
38+
39+
return $delegate;
40+
}
41+
]);
42+
}]);
43+
```
44+
45+
After the `$log` service has been instantiated the decorator is fired. The decorator function has a `$delegate` object
46+
injected to provide access to the service that matches the selector in the decorator. This `$delegate` will be the
47+
service you are decorating.
48+
49+
Decorators have different rules for different services. This is because services are registered in different ways.
50+
Services are selected by name, however filters and directives are selected by appending `"Filter"` or `"Directive"` to
51+
the end of the name. The `$delegate` provided is dictated by the type of service.
52+
53+
| Service Type | Selector | $delegate |
54+
|--------------|-------------------------------|-----------------------------------------------------------------------|
55+
| Service | `serviceName` | The `object` or `function` returned by the service |
56+
| Directive | `directiveName + 'Directive'` | An `Array.<DirectiveObject>`<sub>{@link guide/decorators#drtvArray 1}</sub> |
57+
| Filter | `filterName + 'Filter'` | The `function` returned by the filter |
58+
59+
<small id="drtvArray">1. Multiple directives may be registered to the same selector/name</small>
60+
61+
<div class="alert alert-warning">
62+
**NOTE:** Developers should take care in how and why they are modifying the `$delegate` for the service. Not only
63+
should expectations for the consumer be kept, but some functionality (such as directive registration) does not take
64+
place after decoration, but during creation/registration of the original service. This means, for example, that
65+
an action such as pushing a directive object to a directive `$delegate` will likely result in unexpected behavior.
66+
</div>
67+
68+
## module.decorator
69+
70+
This {@link api/ng/type/angular.Module#decorator function} is the same as the `$provide.decorator` function except it is
71+
exposed through the module API. This allows you to separate your decorator patterns from your module config blocks. The
72+
main caveat here is that you will need to take note the order in which you create your decorators.
73+
74+
Unlike in the module config block (which allows configuration of services prior to their creation), the service must be
75+
registered prior to the decorator (see {@link guide/providers#provider-recipe Provider Recipe}). For example, the
76+
following would not work because you are attempting to decorate outside of the configuration phase and the service
77+
hasn't been created yet:
78+
79+
```js
80+
// will cause an error since 'someService' hasn't been registered
81+
angular.module('myApp').decorator('someService', ...);
82+
83+
angular.module('myApp').factory('someService', ...);
84+
```
85+
86+
## Example Applications
87+
88+
The following sections provide examples each of a service decorator, a directive decorator, and a filter decorator.
89+
90+
### Service Decorator Example
91+
92+
This example shows how we can decorate the `$rootScope` service to add a default value to every scope created in our
93+
app.
94+
95+
<example module="scopeDecorator" name="service-decorator">
96+
<file name="script.js">
97+
angular.module('scopeDecorator', []).
98+
99+
controller('Ctrl', ['$scope', function ($scope) {
100+
$scope.anotherValue = 'Another value for Ctrl\'s scope';
101+
}]).
102+
103+
directive('myDirective', function() {
104+
return {
105+
restrict: 'E',
106+
scope: {},
107+
replace: true,
108+
template: '<p id="myDirective">My directive with an isolate scope: {{ someDefaultValue }}.</p>'
109+
};
110+
}).
111+
112+
config(['$provide', function($provide) {
113+
114+
$provide.decorator('$rootScope', [
115+
'$delegate',
116+
function rootScopeDecorator($delegate) {
117+
118+
// store the old $new fn
119+
var originalNewScopeFn = $delegate.$new;
120+
121+
// create a new $new function that wraps the original
122+
function newScope() {
123+
124+
// create the new child scope and add the default value prior to returning
125+
var newChild = originalNewScopeFn.apply($delegate, arguments);
126+
newChild.someDefaultValue = 'Default value for every scope';
127+
128+
return newChild;
129+
}
130+
131+
// set $new to our new fn
132+
$delegate.$new = newScope;
133+
134+
return $delegate;
135+
}
136+
]);
137+
}]);
138+
</file>
139+
140+
<file name="index.html">
141+
<div ng-controller="Ctrl">
142+
<p id="scopeDecorator">Here is a default value added to every scope: {{ someDefaultValue }}.</p>
143+
<p>Here is a value only on Ctrl's scope: {{ anotherValue }}.</p>
144+
<my-directive></my-directive>
145+
</div>
146+
</file>
147+
148+
<file name="protractor.js" type="protractor">
149+
it('should have default value on scope', function() {
150+
expect(element(by.id('scopeDecorator')).getText())
151+
.toEqual('Here is a default value added to every scope: Default value for every scope.');
152+
expect(element(by.id('myDirective')).getText())
153+
.toEqual('My directive with an isolate scope: Default value for every scope.');
154+
});
155+
</file>
156+
</example>
157+
158+
### Directive Decorator Example
159+
160+
Failed interpolated expressions in `ng-href` attributes can easily go unnoticed. We can decorate `ngHref` to warn us of
161+
those conditions.
162+
163+
<example module="urlDecorator" name="directive-decorator">
164+
<file name="script.js">
165+
angular.module('urlDecorator', []).
166+
167+
controller('Ctrl', ['$scope', function ($scope) {
168+
$scope.id = 3;
169+
}]).
170+
171+
config(['$provide', function($provide) {
172+
173+
// matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions
174+
// from that markup (if they exist) and returns an array of those expressions
175+
function matchExpressions(str) {
176+
var exps = str.match(/{{([^}]+)}}/g);
177+
178+
// if there isn't any, get out of here
179+
if (exps === null) return;
180+
181+
exps = exps.map(function(exp) {
182+
var prop = exp.match(/[^{}]+/);
183+
return prop === null ? null : prop[0];
184+
});
185+
186+
return exps;
187+
}
188+
189+
// remember: directives must be selected by appending 'Directive' to the directive selector
190+
$provide.decorator('ngHrefDirective', [
191+
'$delegate',
192+
'$log',
193+
'$parse',
194+
function($delegate, $log, $parse) {
195+
196+
// store the original link fn
197+
var originalLinkFn = $delegate[0].link;
198+
199+
// replace the compile fn
200+
$delegate[0].compile = function(tElem, tAttr) {
201+
202+
// store the original exp in the directive attribute for our warning message
203+
var originalExp = tAttr.ngHref;
204+
205+
// get the interpolated expressions
206+
var exps = matchExpressions(originalExp);
207+
208+
// create and store the getters using $parse
209+
var getters = exps.map(function(el) {
210+
if (el) return $parse(el);
211+
});
212+
213+
return function newLinkFn(scope, elem, attr) {
214+
// fire the originalLinkFn
215+
originalLinkFn.apply($delegate[0], arguments);
216+
217+
// observe the directive attr and check the expressions
218+
attr.$observe('ngHref', function(val) {
219+
220+
// if we have getters and getters is an array...
221+
if (getters && angular.isArray(getters)) {
222+
223+
// loop through the getters and process them
224+
angular.forEach(getters, function(g, idx) {
225+
226+
// if val is truthy, then the warning won't log
227+
var val = angular.isFunction(g) ? g(scope) : true;
228+
if (!val) {
229+
$log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +
230+
'" is falsy!');
231+
}
232+
233+
});
234+
235+
}
236+
237+
});
238+
239+
};
240+
241+
};
242+
243+
// get rid of the old link function since we return a link function in compile
244+
delete $delegate[0].link;
245+
246+
// return the $delegate
247+
return $delegate;
248+
249+
}
250+
251+
]);
252+
253+
}]);
254+
</file>
255+
256+
<file name="index.html">
257+
<div ng-controller="Ctrl">
258+
<a ng-href="/products/{{ id }}/view" id="id3">View Product {{ id }}</a>
259+
- <strong>id == 3</strong>, so no warning<br>
260+
<a ng-href="/products/{{ id + 5 }}/view" id="id8">View Product {{ id + 5 }}</a>
261+
- <strong>id + 5 == 8</strong>, so no warning<br>
262+
<a ng-href="/products/{{ someOtherId }}/view" id="someOtherId">View Product {{ someOtherId }}</a>
263+
- <strong style="background-color: #ffff00;">someOtherId == undefined</strong>, so warn<br>
264+
<a ng-href="/products/{{ someOtherId + 5 }}/view" id="someOtherId5">View Product {{ someOtherId + 5 }}</a>
265+
- <strong>someOtherId + 5 == 5</strong>, so no warning<br>
266+
</div>
267+
</file>
268+
269+
<file name="protractor.js" type="protractor">
270+
it('should only warn when an expression in the interpolated value is falsy', function() {
271+
var id3 = element(by.id('id3'));
272+
var id8 = element(by.id('id8'));
273+
var someOther = element(by.id('someOtherId'));
274+
var someOther5 = element(by.id('someOtherId5'));
275+
276+
expect(id3.getText()).toEqual('View Product 3');
277+
expect(id3.getAttribute('href')).toContain('/products/3/view');
278+
279+
expect(id8.getText()).toEqual('View Product 8');
280+
expect(id8.getAttribute('href')).toContain('/products/8/view');
281+
282+
expect(someOther.getText()).toEqual('View Product');
283+
expect(someOther.getAttribute('href')).toContain('/products//view');
284+
285+
expect(someOther5.getText()).toEqual('View Product 5');
286+
expect(someOther5.getAttribute('href')).toContain('/products/5/view');
287+
288+
browser.manage().logs().get('browser').then(function(browserLog) {
289+
var loggedMessage = false;
290+
var loggedOnce = true;
291+
292+
browserLog.forEach(function(logObj) {
293+
if (logObj.message.indexOf('NgHref Warning: " someOtherId " ' +
294+
'in the expression "/products/{{ someOtherId }}/view" is falsy!') > -1) {
295+
if (loggedMessage) loggedOnce = false;
296+
loggedMessage = true;
297+
} else if (logObj.message.indexOf('NgHref Warning:') > -1) {
298+
loggedOnce = false;
299+
}
300+
});
301+
302+
expect(loggedMessage).toBe(true);
303+
expect(loggedOnce).toBe(true);
304+
});
305+
});
306+
</file>
307+
</example>
308+
309+
### Filter Decorator Example
310+
311+
Let's say we have created an app that uses the default format for many of our `Date` filters. Suddenly requirements have
312+
changed (that never happens) and we need all of our default dates to be `'shortDate'` instead of `'mediumDate'`.
313+
314+
<example module="filterDecorator" name="filter-decorator">
315+
<file name="script.js">
316+
angular.module('filterDecorator', []).
317+
318+
controller('Ctrl', ['$scope', function ($scope) {
319+
$scope.genesis = new Date(2010, 0, 5);
320+
$scope.ngConf = new Date(2016, 4, 4);
321+
}]).
322+
323+
config(['$provide', function($provide) {
324+
325+
$provide.decorator('dateFilter', [
326+
'$delegate',
327+
function dateDecorator($delegate) {
328+
329+
// store the original filter
330+
var originalFilter = $delegate;
331+
332+
// return our filter
333+
return shortDateDefault;
334+
335+
// shortDateDefault sets the format to shortDate if it is falsy
336+
function shortDateDefault(date, format, timezone) {
337+
if (!format) format = 'shortDate';
338+
339+
// return the result of the original filter
340+
return originalFilter(date, format, timezone);
341+
}
342+
343+
}
344+
345+
]);
346+
347+
}]);
348+
</file>
349+
350+
<file name="index.html">
351+
<div ng-controller="Ctrl">
352+
<div id="genesis">Initial Commit default to short date: {{ genesis | date }}</div>
353+
<div>ng-conf 2016 default short date: {{ ngConf | date }}</div>
354+
<div id="ngConf">ng-conf 2016 with full date format: {{ ngConf | date:'fullDate' }}</div>
355+
</div>
356+
</file>
357+
358+
<file name="protractor.js" type="protractor">
359+
it('should default date filter to short date format', function() {
360+
expect(element(by.id('genesis')).getText()).toContain('Initial Commit default to short date: 1/5/10');
361+
});
362+
363+
it('should still allow dates to be formatted', function() {
364+
expect(element(by.id('ngConf')).getText())
365+
.toContain('ng-conf 2016 with full date format: Wednesday, May 4, 2016');
366+
});
367+
</file>
368+
</example>

0 commit comments

Comments
 (0)