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