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

feat($compile): add more lifecycle hooks to directive controllers #14302

Merged
merged 1 commit into from
Mar 25, 2016

Conversation

petebacondarwin
Copy link
Contributor

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

Feature

What is the current behavior? (You can also link to an open issue here)

Missing lifecycle hooks for directive controllers

What is the new behavior (if this is a feature change)?

Add these lifecycle hooks

Does this PR introduce a breaking change?

Nope

Please check if the PR fulfills these requirements

Other information:

This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

  • $onChanges(changesObj) - Called whenever one-way 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: ... }. 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.
  • $afterViewInit - Called after this controller's element and its children 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.

Closes #14127
Closes #14030
Closes #14020
Closes #13991

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.
Copy link
Member

Choose a reason for hiding this comment

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

Is it also how it happens now if you use regular postLink functions in module.directive-defined directives?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes

I copied and pasted that text from the post link function :-)

Copy link
Member

Choose a reason for hiding this comment

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

TBH, I feel that this last limitation makes this hook pretty usel...ehm...much less useful than the corresponding ng2 hook :(

I assume, with the current implementation, this hook is very similar to the post-link function, which means that:

(a) It doesn't offer new functionality for ng1 users.
(b) It doesn't help much with migrating but for very simple components with inline templates. Even caching the templates won't help, right ?

These are just assumptions atm (I haven't looked at the code yet), but if they are true, I'm a little skeptical about this hook.

Copy link
Member

Choose a reason for hiding this comment

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

In my current project I avoid these problems by not using templateUrl but relying on Webpack being able to import HTML files using the ES6 import syntax. Unfortunately you can't do things like that without any preprocessing done so it can't be a general solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right @gkalpak, which is why I was suggesting that we call it $afterLink instead.
It does, though, have value for components, where there is no way right now to hook into the post-link function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am becoming very keen on the webpack template: require('some.template.html') approach :-)

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I only wish TypeScript had something similar :(
But in the meantime, I believe most developers follow the "best practice" of caching their templates with $templateCache in a build step. Besides a component doesn't know what it's children (or its grandchildren) do wrt template vs templateUrl.

I am afraid this will confuse people and create some headaches while migrating.

If we can't/won't make it work similarly to ng2, it might be btter to name is $postLink and just provide it as a way to access the post-link function while using the .component() helper, as @petebacondarwin suggested.

@mgol
Copy link
Member

mgol commented Mar 23, 2016

I'd add tests that ensure $onChanges is not called on changes in other kinds of bindings.

@petebacondarwin
Copy link
Contributor Author

Fixed the comment typo and added tests @mgol PTAL

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 23, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$afterViewInit` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
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.
* `$afterViewInit` - Called after this controller's element and its children been linked. Similar to the post-link
Copy link
Member

Choose a reason for hiding this comment

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

been linked --> have been linked

@mgol
Copy link
Member

mgol commented Mar 23, 2016

LGTM (once the typo @gkalpak noticed is fixed)

* 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 bindings are updated. The `changesObj` is a hash whose keys
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: I would put the < symbol there somewhere, because @ bindings are also called "one-way" in many blogsposts/tutorials and this might confuse users.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 23, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$afterViewInit` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 23, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$afterViewInit` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302

function triggerOnChangesHook() {
// We must run this hook in an apply since the $$postDigest runs outside apply
scope.$apply(function() {
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't this mean there will be one extra digest for each controller (that has at least one changed binding) per digest ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes you are right.
The alternative is to use $applyAsync but this has the downside of delaying the call to $onChanges by maybe 100ms.

What is better?

Copy link
Member

Choose a reason for hiding this comment

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

I think we could move the login that calls triggerOnChangeHook inside $compile (I mean make it a compiler-local thing) and store the changes for each controller there, so we only trigger one extra $digest.

@gkalpak
Copy link
Member

gkalpak commented Mar 23, 2016

I am also wondering if applying $onChange to @ bindings would be closer to ng2 (iirc it will).

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 23, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 23, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
@@ -147,6 +147,30 @@ components should follow a few simple conventions:
}
```

- **Components have a well-defined lifecycle
Copy link
Member

Choose a reason for hiding this comment

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

Missing closing **.

function flushOnChangesQueue() {
// We must run this hook in an apply since the $$postDigest runs outside apply
$rootScope.$apply(function() {
for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
Copy link
Member

Choose a reason for hiding this comment

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

What about changes detected during this digest ?
We should either not hard code the length or collect them and run another digest (but then there's a risk for infinite loops).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call...
All the items in the onChanges queue will be flushed before the new digest is triggered.

The sequence looks like:

  • some change 1
  • some change 2
  • $digest
    • schedule flushOnChangesQueue
    • queue onChanges 1
    • queue onChanges 2
  • $$postDigest
    • flushOnChangesQueue
      • $apply
        • trigger onChanges 1
          • some change 1a
        • trigger onChanges 2
          • some change 2a
        • $digest
          • schedule onChangesFlush
          • queue onChanges 1
          • queue onChanges 2
        • $$postDigest
          • flushOnChangesQueue
            • $apply
              • trigger onChanges 1
              • trigger onChanges 2
            • $digest

I think this means that we are OK (except for the potential for deep recursion). I'll make a test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a test @gkalpak PTAL

Copy link
Member

Choose a reason for hiding this comment

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

So, this looks good apart from the infinite loop issue.

Ideally, there should be some mechanism in place to prevent freezing the whole app, because someone misused $onChanges (a TTL-like mechanism maybe).
If not (e.g. it turns out to be too complicated), we should at least document very clearly the dangers of making changes that would trigger another $onChanges round. from inside an $onChanges() callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not an infinite loop. It is an infinite recursion, which will trigger a RangeError exception that will prevent the browser from freezing and indicate to the developer that they have done a bad thing.

Added a test to demonstrate.

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 23, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 23, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
* 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 (`<`) bindings are updated. The `changesObj` is a hash
Copy link
Member

Choose a reason for hiding this comment

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

Now we need to change this again to also mention @ 😁

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 23, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
@gkalpak
Copy link
Member

gkalpak commented Mar 23, 2016

LGTM with this comment (somehow) addressed
👍

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 24, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
petebacondarwin added a commit to petebacondarwin/angular.js that referenced this pull request Mar 24, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
@petebacondarwin
Copy link
Contributor Author

I've added time-to-live constraints for onChanges hooks, which should address @gkalpak's last concern

<c1 prop="a" on-change="a = -a"></c1>
```

```js
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add the component definition object? Makes it easier to understand if you know the bindings are bindings: {'prop': '<', onChange: '&'}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes angular#14127
Closes angular#14030
Closes angular#14020
Closes angular#13991
Closes angular#14302
@petebacondarwin petebacondarwin merged commit 874c0fd into angular:master Mar 25, 2016
petebacondarwin added a commit that referenced this pull request Mar 25, 2016
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way 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: ... }`. 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.
 * `$postLink` - Called after this controller's element and its children 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.

Closes #14127
Closes #14030
Closes #14020
Closes #13991
Closes #14302
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.