From 8f63c5aeb08b0fd2eb71020be057fdfc429aa1d6 Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Mon, 23 May 2016 18:31:21 -0700 Subject: [PATCH 1/5] feat($compile): backport $doCheck Backuport ngDoCheck from Angular 2. --- src/ng/compile.js | 23 ++++++++++++++++++++- test/ng/compileSpec.js | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 9940cd4bde2f..747ca500afc3 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -300,6 +300,12 @@ * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a * component such as cloning the bound value to prevent accidental mutation of the outer value. + * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on + * changes. Any actions that you wish to take in response to the changes that you detect must be + * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook + * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not + * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments; + * if detecting changes, you must store the previous value(s) for comparison to the current values. * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent @@ -2499,6 +2505,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { $exceptionHandler(e); } } + if (isFunction(controllerInstance.$doCheck)) { + controllerInstance.$doCheck(); + } + if (isFunction(controllerInstance.$doCheck)) { + controllerInstance.$doCheck(); + } if (isFunction(controllerInstance.$onDestroy)) { controllerScope.$on('$destroy', function callOnDestroyHook() { controllerInstance.$onDestroy(); @@ -3151,7 +3163,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { forEach(bindings, function initializeBinding(definition, scopeName) { var attrName = definition.attrName, optional = definition.optional, - mode = definition.mode, // @, =, or & + mode = definition.mode, // @, =, <, or & lastValue, parentGet, parentSet, compare, removeWatch; @@ -3263,6 +3275,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }); + if (isFunction(destination.$doCheck)) { + var doCheckWatch = scope.$watch(triggerDoCheckHook); + removeWatchCollection.push(doCheckWatch); + } + function recordChanges(key, currentValue, previousValue) { if (isFunction(destination.$onChanges) && currentValue !== previousValue) { // If we have not already scheduled the top level onChangesQueue handler then do so now @@ -3290,6 +3307,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { changes = undefined; } + function triggerDoCheckHook() { + destination.$doCheck(); + } + return { initialChanges: initialChanges, removeWatches: removeWatchCollection.length && function removeWatches() { diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index e8d9d277ea99..32233826648d 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -3828,6 +3828,51 @@ describe('$compile', function() { }); }); + describe('$doCheck', function() { + it('should call `$doCheck`, if provided, for each digest cycle, after $onChanges and $onInit', function() { + var log = []; + + function TestController() { } + TestController.prototype.$doCheck = function() { log.push('$doCheck'); }; + TestController.prototype.$onChanges = function() { log.push('$onChanges'); }; + TestController.prototype.$onInit = function() { log.push('$onInit'); }; + + angular.module('my', []) + .component('dcc', { + controller: TestController, + bindings: { 'prop1': '<' } + }); + + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual([ + '$onChanges', + '$onInit', + '$doCheck' + ]); + + // Clear log + log = []; + + $rootScope.$apply(); + expect(log).toEqual([ + '$doCheck', + '$doCheck' + ]); + + // Clear log + log = []; + + $rootScope.$apply('val = 2'); + expect(log).toEqual([ + '$doCheck', + '$onChanges', + '$doCheck' + ]); + }); + }); + }); describe('$onChanges', function() { From 5096e343b4ff339ebeeaafc2ebfab650c0342a62 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 21 Jun 2016 10:49:18 +0100 Subject: [PATCH 2/5] fix($compile): ensure `$doCheck` hooks can be defined in the controller constructor --- src/ng/compile.js | 46 +++++++++++++++++++++++++++++++----------- test/ng/compileSpec.js | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 747ca500afc3..1f9069a3fd03 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -316,6 +316,39 @@ * they are waiting for their template to load asynchronously and their own compilation and linking has been * suspended until that occurs. * + * ** $doCheck example ** + * + * This example show how you might use `$doCheck` to customise the equality check of component inputs. + * + * + * + *
+ * + * + *
{{ items }}
+ * + *
+ *
+ * + * angular.module('do-check-module', []) + * .component('test', { + * bindings: { items: '<' }, + * template: + * '
{{ $ctrl.log | json }}
', + * controller: function() { + * this.log = []; + * + * this.$doCheck = function() { + * if (this.items_ref !== this.items) { this.log.push('doCheck: items changed'); } + * if (!angular.equals(this.items_clone, this.items)) { this.log.push('doCheck: items mutated'); } + * + * this.items_clone = angular.copy(this.items); + * this.items_ref = this.items; + * }; + * } + * }); + *
+ *
* * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The @@ -2506,9 +2539,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } if (isFunction(controllerInstance.$doCheck)) { - controllerInstance.$doCheck(); - } - if (isFunction(controllerInstance.$doCheck)) { + controllerScope.$watch(function() { controllerInstance.$doCheck(); }); controllerInstance.$doCheck(); } if (isFunction(controllerInstance.$onDestroy)) { @@ -3275,11 +3306,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }); - if (isFunction(destination.$doCheck)) { - var doCheckWatch = scope.$watch(triggerDoCheckHook); - removeWatchCollection.push(doCheckWatch); - } - function recordChanges(key, currentValue, previousValue) { if (isFunction(destination.$onChanges) && currentValue !== previousValue) { // If we have not already scheduled the top level onChangesQueue handler then do so now @@ -3307,10 +3333,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { changes = undefined; } - function triggerDoCheckHook() { - destination.$doCheck(); - } - return { initialChanges: initialChanges, removeWatches: removeWatchCollection.length && function removeWatches() { diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 32233826648d..6acc2ec6f7a6 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -3872,6 +3872,51 @@ describe('$compile', function() { ]); }); }); + + it('should work if $doCheck is provided in the constructor', function() { + var log = []; + + function TestController() { + this.$doCheck = function() { log.push('$doCheck'); }; + this.$onChanges = function() { log.push('$onChanges'); }; + this.$onInit = function() { log.push('$onInit'); }; + } + + angular.module('my', []) + .component('dcc', { + controller: TestController, + bindings: { 'prop1': '<' } + }); + + module('my'); + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + expect(log).toEqual([ + '$onChanges', + '$onInit', + '$doCheck' + ]); + + // Clear log + log = []; + + $rootScope.$apply(); + expect(log).toEqual([ + '$doCheck', + '$doCheck' + ]); + + // Clear log + log = []; + + $rootScope.$apply('val = 2'); + expect(log).toEqual([ + '$doCheck', + '$onChanges', + '$doCheck' + ]); + }); + }); }); describe('$onChanges', function() { From b409bed16c931bf565d3656fc52ff0c19910a811 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Tue, 21 Jun 2016 18:57:31 +0100 Subject: [PATCH 3/5] docs($compile): add additional runnable examples for the `$doCheck` hook --- src/ng/compile.js | 62 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 1f9069a3fd03..b45e5543d633 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -316,11 +316,54 @@ * they are waiting for their template to load asynchronously and their own compilation and linking has been * suspended until that occurs. * - * ** $doCheck example ** + * ** $doCheck examples ** * - * This example show how you might use `$doCheck` to customise the equality check of component inputs. + * This example shows how you can check for mutations to a Date object even though the identity of the object + * has not changed. * - * + * + * + * angular.module('do-check-module', []) + * .component('app', { + * template: + * 'Month: ' + + * 'Date: {{ $ctrl.date }}' + + * '', + * controller: function() { + * this.date = new Date(); + * this.month = this.date.getMonth(); + * this.updateDate = function() { + * this.date.setMonth(this.month); + * }; + * } + * }) + * .component('test', { + * bindings: { date: '<' }, + * template: + * '
{{ $ctrl.log | json }}
', + * controller: function() { + * var previousValue; + * this.log = []; + * this.$doCheck = function() { + * var currentValue = this.date && this.date.valueOf(); + * if (previousValue !== currentValue) { + * this.log.push('doCheck: date mutated: ' + this.date); + * previousValue = currentValue; + * } + * }; + * } + * }); + *
+ * + * + * + *
+ * + * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the + * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large + * arrays or objects can have a negative impact on your application performance) + * + * * *
* @@ -339,11 +382,14 @@ * this.log = []; * * this.$doCheck = function() { - * if (this.items_ref !== this.items) { this.log.push('doCheck: items changed'); } - * if (!angular.equals(this.items_clone, this.items)) { this.log.push('doCheck: items mutated'); } - * - * this.items_clone = angular.copy(this.items); - * this.items_ref = this.items; + * if (this.items_ref !== this.items) { + * this.log.push('doCheck: items changed'); + * this.items_ref = this.items; + * } + * if (!angular.equals(this.items_clone, this.items)) { + * this.log.push('doCheck: items mutated'); + * this.items_clone = angular.copy(this.items); + * } * }; * } * }); From 69be7a99d8a249af8868436c6f452daa9122a2c8 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 22 Jun 2016 10:16:55 +0100 Subject: [PATCH 4/5] docs($compile): reorganize the life-cycle hooks docs --- src/ng/compile.js | 228 +++++++++++++++++++++++++--------------------- 1 file changed, 122 insertions(+), 106 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index b45e5543d633..68e49f2bcaf1 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -52,8 +52,9 @@ * There are many different options for a directive. * * The difference resides in the return value of the factory function. - * You can either return a "Directive Definition Object" (see below) that defines the directive properties, - * or just the `postLink` function (all other properties will have the default values). + * You can either return a {@link $compile#directive-definition-object Directive Definition Object (see below)} + * that defines the directive properties, or just the `postLink` function (all other properties will have + * the default values). * *
* **Best Practice:** It's recommended to use the "directive definition object" form. @@ -117,6 +118,125 @@ * }); * ``` * + * ### Life-cycle hooks + * Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the + * directive: + * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and + * had their bindings initialized (and before the pre & post linking functions for the directives on + * this element). This is a good place to put initialization code for your controller. + * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The + * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an + * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a + * component such as cloning the bound value to prevent accidental mutation of the outer value. + * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on + * changes. Any actions that you wish to take in response to the changes that you detect must be + * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook + * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not + * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments; + * if detecting changes, you must store the previous value(s) for comparison to the current values. + * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing + * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in + * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent + * components will have their `$onDestroy()` hook called before child components. + * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link + * function this hook can be used to set up DOM event handlers and do direct DOM manipulation. + * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since + * they are waiting for their template to load asynchronously and their own compilation and linking has been + * suspended until that occurs. + * + * #### Comparison with Angular 2 life-cycle hooks + * Angular 2 also uses life-cycle hooks for its components. While the Angular 1 life-cycle hooks are similar there are + * some differences that you should be aware of, especially when it comes to moving your code from Angular 1 to Angular 2: + * + * * Angular 1 hooks are prefixed with `$`, such as `$onInit`. Angular 2 hooks are prefixed with `ng`, such as `ngOnInit`. + * * Angular 1 hooks can be defined on the controller prototype or added to the controller inside its constructor. + * In Angular 2 you can only define hooks on the prototype of the Component class. + * * Due to the differences in change-detection, you may get many more calls to `$doCheck` in Angular 1 than you would to + * `ngDoCheck` in Angular 2 + * * Changes to the model inside `$doCheck` will trigger new turns of the digest loop, which will cause the changes to be + * propagated throughout the application. + * Angular 2 does not allow the `ngDoCheck` hook to trigger a change outside of the component. It will either throw an + * error or do nothing depending upon the state of `enableProdMode()`. + * + * #### Life-cycle hook examples + * + * This example shows how you can check for mutations to a Date object even though the identity of the object + * has not changed. + * + * + * + * angular.module('do-check-module', []) + * .component('app', { + * template: + * 'Month: ' + + * 'Date: {{ $ctrl.date }}' + + * '', + * controller: function() { + * this.date = new Date(); + * this.month = this.date.getMonth(); + * this.updateDate = function() { + * this.date.setMonth(this.month); + * }; + * } + * }) + * .component('test', { + * bindings: { date: '<' }, + * template: + * '
{{ $ctrl.log | json }}
', + * controller: function() { + * var previousValue; + * this.log = []; + * this.$doCheck = function() { + * var currentValue = this.date && this.date.valueOf(); + * if (previousValue !== currentValue) { + * this.log.push('doCheck: date mutated: ' + this.date); + * previousValue = currentValue; + * } + * }; + * } + * }); + *
+ * + * + * + *
+ * + * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the + * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large + * arrays or objects can have a negative impact on your application performance) + * + * + * + *
+ * + * + *
{{ items }}
+ * + *
+ *
+ * + * angular.module('do-check-module', []) + * .component('test', { + * bindings: { items: '<' }, + * template: + * '
{{ $ctrl.log | json }}
', + * controller: function() { + * this.log = []; + * + * this.$doCheck = function() { + * if (this.items_ref !== this.items) { + * this.log.push('doCheck: items changed'); + * this.items_ref = this.items; + * } + * if (!angular.equals(this.items_clone, this.items)) { + * this.log.push('doCheck: items mutated'); + * this.items_clone = angular.copy(this.items); + * } + * }; + * } + * }); + *
+ *
* * * ### Directive Definition Object @@ -292,110 +412,6 @@ * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns * `true` if the specified slot contains content (i.e. one or more DOM nodes). * - * The controller can provide the following methods that act as life-cycle hooks: - * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and - * had their bindings initialized (and before the pre & post linking functions for the directives on - * this element). This is a good place to put initialization code for your controller. - * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The - * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an - * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a - * component such as cloning the bound value to prevent accidental mutation of the outer value. - * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on - * changes. Any actions that you wish to take in response to the changes that you detect must be - * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook - * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not - * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments; - * if detecting changes, you must store the previous value(s) for comparison to the current values. - * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing - * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in - * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent - * components will have their `$onDestroy()` hook called before child components. - * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link - * function this hook can be used to set up DOM event handlers and do direct DOM manipulation. - * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since - * they are waiting for their template to load asynchronously and their own compilation and linking has been - * suspended until that occurs. - * - * ** $doCheck examples ** - * - * This example shows how you can check for mutations to a Date object even though the identity of the object - * has not changed. - * - * - * - * angular.module('do-check-module', []) - * .component('app', { - * template: - * 'Month: ' + - * 'Date: {{ $ctrl.date }}' + - * '', - * controller: function() { - * this.date = new Date(); - * this.month = this.date.getMonth(); - * this.updateDate = function() { - * this.date.setMonth(this.month); - * }; - * } - * }) - * .component('test', { - * bindings: { date: '<' }, - * template: - * '
{{ $ctrl.log | json }}
', - * controller: function() { - * var previousValue; - * this.log = []; - * this.$doCheck = function() { - * var currentValue = this.date && this.date.valueOf(); - * if (previousValue !== currentValue) { - * this.log.push('doCheck: date mutated: ' + this.date); - * previousValue = currentValue; - * } - * }; - * } - * }); - *
- * - * - * - *
- * - * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the - * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large - * arrays or objects can have a negative impact on your application performance) - * - * - * - *
- * - * - *
{{ items }}
- * - *
- *
- * - * angular.module('do-check-module', []) - * .component('test', { - * bindings: { items: '<' }, - * template: - * '
{{ $ctrl.log | json }}
', - * controller: function() { - * this.log = []; - * - * this.$doCheck = function() { - * if (this.items_ref !== this.items) { - * this.log.push('doCheck: items changed'); - * this.items_ref = this.items; - * } - * if (!angular.equals(this.items_clone, this.items)) { - * this.log.push('doCheck: items mutated'); - * this.items_clone = angular.copy(this.items); - * } - * }; - * } - * }); - *
- *
- * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The * `require` property can be a string, an array or an object: From 2dcbc4e2f3160b69826da64297e67d5073de87f4 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Wed, 22 Jun 2016 10:17:05 +0100 Subject: [PATCH 5/5] docs($http): fix a dangling link --- src/ng/http.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/http.js b/src/ng/http.js index 7da80bccc92c..b90e16e90dfb 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -1123,7 +1123,7 @@ function $HttpProvider() { * @description * Shortcut method to perform `JSONP` request. * If you would like to customise where and how the callbacks are stored then try overriding - * or decorating the {@link jsonpCallbacks} service. + * or decorating the {@link $jsonpCallbacks} service. * * @param {string} url Relative or absolute URL specifying the destination of the request. * The name of the callback should be the string `JSON_CALLBACK`.