Skip to content

Commit 013098e

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 013098e

File tree

1 file changed

+356
-0
lines changed

1 file changed

+356
-0
lines changed

docs/content/guide/decorators.ngdoc

+356
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
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+
$scope.warnCount = 0; // for testing
170+
}]).
171+
172+
config(['$provide', function($provide) {
173+
174+
// matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions
175+
// from that markup (if they exist) and returns an array of those expressions
176+
function matchExpressions(str) {
177+
var exps = str.match(/{{([^}]+)}}/g);
178+
179+
// if there isn't any, get out of here
180+
if (exps === null) return;
181+
182+
exps = exps.map(function(exp) {
183+
var prop = exp.match(/[^{}]+/);
184+
return prop === null ? null : prop[0];
185+
});
186+
187+
return exps;
188+
}
189+
190+
// remember: directives must be selected by appending 'Directive' to the directive selector
191+
$provide.decorator('ngHrefDirective', [
192+
'$delegate',
193+
'$log',
194+
'$parse',
195+
function($delegate, $log, $parse) {
196+
197+
// store the original link fn
198+
var originalLinkFn = $delegate[0].link;
199+
200+
// replace the compile fn
201+
$delegate[0].compile = function(tElem, tAttr) {
202+
203+
// store the original exp in the directive attribute for our warning message
204+
var originalExp = tAttr.ngHref;
205+
206+
// get the interpolated expressions
207+
var exps = matchExpressions(originalExp);
208+
209+
// create and store the getters using $parse
210+
var getters = exps.map(function(el) {
211+
if (el) return $parse(el);
212+
});
213+
214+
return function newLinkFn(scope, elem, attr) {
215+
// fire the originalLinkFn
216+
originalLinkFn.apply($delegate[0], arguments);
217+
218+
// observe the directive attr and check the expressions
219+
attr.$observe('ngHref', function(val) {
220+
221+
// if we have getters and getters is an array...
222+
if (getters && angular.isArray(getters)) {
223+
224+
// loop through the getters and process them
225+
angular.forEach(getters, function(g, idx) {
226+
227+
// if val is truthy, then the warning won't log
228+
var val = angular.isFunction(g) ? g(scope) : true;
229+
if (!val) {
230+
$log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +
231+
'" is falsy!');
232+
233+
scope.warnCount++; // for testing
234+
}
235+
236+
});
237+
238+
}
239+
240+
});
241+
242+
};
243+
244+
};
245+
246+
// get rid of the old link function since we return a link function in compile
247+
delete $delegate[0].link;
248+
249+
// return the $delegate
250+
return $delegate;
251+
252+
}
253+
254+
]);
255+
256+
}]);
257+
</file>
258+
259+
<file name="index.html">
260+
<div ng-controller="Ctrl">
261+
<a ng-href="/products/{{ id }}/view" id="id3">View Product {{ id }}</a>
262+
- <strong>id == 3</strong>, so no warning<br>
263+
<a ng-href="/products/{{ id + 5 }}/view" id="id8">View Product {{ id + 5 }}</a>
264+
- <strong>id + 5 == 8</strong>, so no warning<br>
265+
<a ng-href="/products/{{ someOtherId }}/view" id="someOtherId">View Product {{ someOtherId }}</a>
266+
- <strong style="background-color: #ffff00;">someOtherId == undefined</strong>, so warn<br>
267+
<a ng-href="/products/{{ someOtherId + 5 }}/view" id="someOtherId5">View Product {{ someOtherId + 5 }}</a>
268+
- <strong>someOtherId + 5 == 5</strong>, so no warning<br>
269+
<div>Warn Count: {{ warnCount }}</div>
270+
</div>
271+
</file>
272+
273+
<file name="protractor.js" type="protractor">
274+
it('should warn when an expression in the interpolated value is falsy', function() {
275+
var id3 = element(by.id('id3'));
276+
var id8 = element(by.id('id8'));
277+
var someOther = element(by.id('someOtherId'));
278+
var someOther5 = element(by.id('someOtherId5'));
279+
280+
expect(id3.getText()).toEqual('View Product 3');
281+
expect(id3.getAttribute('href')).toContain('/products/3/view');
282+
283+
expect(id8.getText()).toEqual('View Product 8');
284+
expect(id8.getAttribute('href')).toContain('/products/8/view');
285+
286+
expect(someOther.getText()).toEqual('View Product');
287+
expect(someOther.getAttribute('href')).toContain('/products//view');
288+
289+
expect(someOther5.getText()).toEqual('View Product 5');
290+
expect(someOther5.getAttribute('href')).toContain('/products/5/view');
291+
292+
expect(element(by.binding('warnCount')).getText()).toEqual('Warn Count: 1');
293+
});
294+
</file>
295+
</example>
296+
297+
### Filter Decorator Example
298+
299+
Let's say we have created an app that uses the default format for many of our `Date` filters. Suddenly requirements have
300+
changed (that never happens) and we need all of our default dates to be `'shortDate'` instead of `'mediumDate'`.
301+
302+
<example module="filterDecorator" name="filter-decorator">
303+
<file name="script.js">
304+
angular.module('filterDecorator', []).
305+
306+
controller('Ctrl', ['$scope', function ($scope) {
307+
$scope.genesis = new Date(2010, 0, 5);
308+
$scope.ngConf = new Date(2016, 4, 4);
309+
}]).
310+
311+
config(['$provide', function($provide) {
312+
313+
$provide.decorator('dateFilter', [
314+
'$delegate',
315+
function dateDecorator($delegate) {
316+
317+
// store the original filter
318+
var originalFilter = $delegate;
319+
320+
// return our filter
321+
return shortDateDefault;
322+
323+
// shortDateDefault sets the format to shortDate if it is falsy
324+
function shortDateDefault(date, format, timezone) {
325+
if (!format) format = 'shortDate';
326+
327+
// return the result of the original filter
328+
return originalFilter(date, format, timezone);
329+
}
330+
331+
}
332+
333+
]);
334+
335+
}]);
336+
</file>
337+
338+
<file name="index.html">
339+
<div ng-controller="Ctrl">
340+
<div id="genesis">Initial Commit default to short date: {{ genesis | date }}</div>
341+
<div>ng-conf 2016 default short date: {{ ngConf | date }}</div>
342+
<div id="ngConf">ng-conf 2016 with full date format: {{ ngConf | date:'fullDate' }}</div>
343+
</div>
344+
</file>
345+
346+
<file name="protractor.js" type="protractor">
347+
it('should default date filter to short date format', function() {
348+
expect(element(by.id('genesis')).getText()).toContain('Initial Commit default to short date: 1/5/10');
349+
});
350+
351+
it('should still allow dates to be formatted', function() {
352+
expect(element(by.id('ngConf')).getText())
353+
.toContain('ng-conf 2016 with full date format: Wednesday, May 4, 2016');
354+
});
355+
</file>
356+
</example>

0 commit comments

Comments
 (0)