Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Pr 14656 #14811

Closed
wants to merge 5 commits into from
Closed

Pr 14656 #14811

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 127 additions & 22 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
* <div class="alert alert-success">
* **Best Practice:** It's recommended to use the "directive definition object" form.
Expand Down Expand Up @@ -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 &amp; 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.
*
* <example name="doCheckDateExample" module="do-check-module">
* <file name="app.js">
* angular.module('do-check-module', [])
* .component('app', {
* template:
* 'Month: <input ng-model="$ctrl.month" ng-change="$ctrl.updateDate()">' +
* 'Date: {{ $ctrl.date }}' +
* '<test date="$ctrl.date"></test>',
* controller: function() {
* this.date = new Date();
* this.month = this.date.getMonth();
* this.updateDate = function() {
* this.date.setMonth(this.month);
* };
* }
* })
* .component('test', {
* bindings: { date: '<' },
* template:
* '<pre>{{ $ctrl.log | json }}</pre>',
* 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;
* }
* };
* }
* });
* </file>
* <file name="index.html">
* <app></app>
* </file>
* </example>
*
* 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)
*
* <example name="doCheckArrayExample" module="do-check-module">
* <file name="index.html">
* <div ng-init="items = []">
* <button ng-click="items.push(items.length)">Add Item</button>
* <button ng-click="items = []">Reset Items</button>
* <pre>{{ items }}</pre>
* <test items="items"></test>
* </div>
* </file>
* <file name="app.js">
* angular.module('do-check-module', [])
* .component('test', {
* bindings: { items: '<' },
* template:
* '<pre>{{ $ctrl.log | json }}</pre>',
* 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);
* }
* };
* }
* });
* </file>
* </example>
*
*
* ### Directive Definition Object
Expand Down Expand Up @@ -292,25 +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 &amp; 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.
* * `$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.
*
*
* #### `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:
Expand Down Expand Up @@ -2499,6 +2600,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
$exceptionHandler(e);
}
}
if (isFunction(controllerInstance.$doCheck)) {
controllerScope.$watch(function() { controllerInstance.$doCheck(); });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to manually track the remove watch function because we are after the point when we might swap out the controller for another instance. This watch will naturally be removed when the controller scope is destroyed.

controllerInstance.$doCheck();
}
if (isFunction(controllerInstance.$onDestroy)) {
controllerScope.$on('$destroy', function callOnDestroyHook() {
controllerInstance.$onDestroy();
Expand Down Expand Up @@ -3151,7 +3256,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;

Expand Down
2 changes: 1 addition & 1 deletion src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
90 changes: 90 additions & 0 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3828,6 +3828,96 @@ 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('<dcc prop1="val"></dcc>')($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'
]);
});
});

it('should work if $doCheck is provided in the constructor', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that we have a test that verifies our incompatibility wiht ng2 😛

Copy link
Contributor Author

@petebacondarwin petebacondarwin Jun 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More ribbing? :-P
It is a common pattern in ng1, where classes are not the be-all-and-end-all, to put methods on the instance :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No ribbing intended 😃
It's a good thing that we have this test. It's not ideal that we are incompatible with ng2, but it's much better that being incompatible with ng1 users and common patterns 😉

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test looks identical to the one I added above?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test above assigns the methods on the prototype. This test defines them on the instance from inside the constructor.
(Note that while both work in ng1, in ng2 only the prototype will work.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha okay, thanks for explaining :)

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('<dcc prop1="val"></dcc>')($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() {

Expand Down