Skip to content

fix(elements): initialization should run in ngZone (#24181)#24185

Closed
remackgeek wants to merge 11 commits intoangular:masterfrom
remackgeek:elements-connect-zone
Closed

fix(elements): initialization should run in ngZone (#24181)#24185
remackgeek wants to merge 11 commits intoangular:masterfrom
remackgeek:elements-connect-zone

Conversation

@remackgeek
Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

[x ] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] angular.io application / infrastructure changes
[ ] Other... Please describe:

What is the current behavior?

Issue Number: #24181
When an angular element is created outside the angular zone, such as in setTimeout(), automatic change detection no longer works

What is the new behavior?

during connect(), initializeComponent is always called in the angular zone

Does this PR introduce a breaking change?

[ ] Yes
[ x] No

Other information

@JiaLiPassion this is based/extends on your PR 23885, please review

@JiaLiPassion
Copy link
Copy Markdown
Contributor

@remackgeek , sure, I think a demo to describe this issue will be helpful. So your use case is you will create the Angular Elements dynamically with such as setTimeout?

@remackgeek
Copy link
Copy Markdown
Contributor Author

remackgeek commented May 30, 2018 via email

Copy link
Copy Markdown
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

Wouldn't the same issue affect things like detectChanges() and destroy()?

@vicb vicb added the area: elements Issues related to Angular Elements label May 31, 2018
@remackgeek
Copy link
Copy Markdown
Contributor Author

@gkalpak, yes. Please see the latest update -- if you choose zone behavior, this wraps all the strategy methods.

this.insureAngularZone(() => { super.connect(element); });
}

disconnect() { this.insureAngularZone(super.disconnect); }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Wouldn't this cause this to be undefined inside super.disconnect?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yeah, and its inconsistent with the line above it.

}

/**
* extends ComponentNgElementStrategy to insure callbacks run in the Angular zone
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

*ensure

this.insureAngularZone(() => { super.setInputValue(propName, value); });
}

private insureAngularZone(fn: () => any) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

*ensure

would this be better named as runInZone or similar?

this.insureAngularZone(() => { super.connect(element); });
}

disconnect() { this.insureAngularZone(super.disconnect); }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yeah, and its inconsistent with the line above it.

@remackgeek
Copy link
Copy Markdown
Contributor Author

@gkalpak, @robwormald thanks for the review. Updated.

@LinboLen
Copy link
Copy Markdown

I picked ComponentNgElementZoneStrategyFactory out. and it works.

@krawetko
Copy link
Copy Markdown

krawetko commented Jul 24, 2018

@robwormald @remackgeek @gkalpak what are your plans for this change? My company has built many Angular Components and having the opportunity to export them as WebComponent to integrate in other non Angular applications (with Zones/Change Detection working out of the box) would be a huge use case and argument to move a lot of development focus into Angular Components.

@kseamon
Copy link
Copy Markdown
Contributor

kseamon commented Aug 14, 2018

Does this ensure that external callbacks attached to our @outputs run outside of the Angular zone? I'd think that's desirable for performance and encapsulation reasons.

The existing code in this PR would ensure that we re-enter the zone on @input, so it should be safe as well.

export class ComponentNgElementZoneStrategy extends ComponentNgElementStrategy {
private ngZone: NgZone;

constructor(protected componentFactory: ComponentFactory<any>, protected injector: Injector) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No need for these to be protected here - the superclass already takes care of assigning them to itself.

private readonly uninitializedInputs = new Set<string>();

constructor(private componentFactory: ComponentFactory<any>, private injector: Injector) {}
constructor(protected componentFactory: ComponentFactory<any>, protected injector: Injector) {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

protected readonly

* @experimental
*/
export class ComponentNgElementZoneStrategy extends ComponentNgElementStrategy {
private ngZone: NgZone;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

readonly

@kseamon
Copy link
Copy Markdown
Contributor

kseamon commented Aug 14, 2018

To clarify my output runOutsideAngular suggestion, it would look something like this:

initializeOutputs() {
  // Sets this.events
  super.initializeOutputs();

  this.events = this.events.pipe(runOutsideAngularZone(this.ngZone));
}

function runOutsideAngularZone(ngZone: NgZone) {
  return <T>(source: Observable<T>) => NgZone.isInAngularZone() ?
      new Observable<T>((observer) => source.subscribe({
           next: (value) => ngZone.runOutsideAngular(() => observer.next(value)),
           error: (err) => observer.error(err),
           complete: () => observer.complete()
         })) : 
      source;
}

@remackgeek
Copy link
Copy Markdown
Contributor Author

@kseamon, wouldn't there be cases where you want the event to be in the zone? Say a third-party element being used in an Angular SPA.

@kseamon
Copy link
Copy Markdown
Contributor

kseamon commented Aug 20, 2018 via email

this.runInZone(() => { super.setInputValue(propName, value); });
}

private runInZone(fn: () => any) { return NgZone.isInAngularZone() ? fn() : this.ngZone.run(fn); }
Copy link
Copy Markdown

@tnicola tnicola Aug 20, 2018

Choose a reason for hiding this comment

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

Remove NgZone.isInAngularZone() check. If we are are in a micro-frondend situation (an Angular shell app plus some angular elements imported from different js angular bundles), this condition will return true, because we always are in a AngularZone, but the wrong one to perfom change detection: the shell, we need to run the code in the element zone instead, in order to get the change detection ( AppReference.tick()) working for the element.

On the other hand, in a normal situation with just one angular shell and elements created from the inside, it won't be a problem to call ngZone.run(), it should cost zero, so the "zone check" could be safely removed.

You can find the PoC and more details in this PR: remackgeek/elements-zone-strategy#2.

@kseamon
Copy link
Copy Markdown
Contributor

kseamon commented Aug 20, 2018

Per @tnicola's comment, each child app would have it's own ngZone, so leaving it should be fine and would not cause the parent app to leave its own zone.

@JiaLiPassion
Copy link
Copy Markdown
Contributor

@remackgeek, yeah, I am working on a scoped zone in zone.js, so in theory, we can have separate zone per Angular Element. angular/zone.js#1126

@remackgeek
Copy link
Copy Markdown
Contributor Author

So, to summarize the various threads to make sure I'm following everything:

  1. To support the multiple ngZone case, the wrappers should always call ngZone.run(), because the NgZone.isInAngularZone() check doesn't distinguish between ngZones. This will add performance overhead to the case where the wrapper is already in the correct ngZone. PR 1126 should address this.

  2. In all cases, the strategy should avoid leaking it's ngZone in callbacks, so the connect wrapper should capture Zone.current before running in the ngZone and run the callback events in that zone.

This should handle the 3 broad use cases:

  1. The outer zone is the same ngZone as the element -- both the element and the host belong to the same angular application.
  2. The outer zone is the root zone -- the element was created in a setTimeout() or similar.
  3. The outer zone is another ngZone -- the host is an angular app consuming separately built angular elements.

@remackgeek
Copy link
Copy Markdown
Contributor Author

@tnicola, I updated the PR to check the specific ngZone while waiting for PR 1126 land

@kseamon, I did some testing -- the event handlers for the outputs already run in the zone they were created in, so I'm not sure where there would be a performance gain changing zones in the Observable. But the mechanics of zones are black magic to me, so let me know if I missed something.

@kseamon
Copy link
Copy Markdown
Contributor

kseamon commented Sep 4, 2018 via email

@JiaLiPassion
Copy link
Copy Markdown
Contributor

I created a PR in zone.js here, angular/zone.js#1133, it will patch all CustomElement v1 APIs, so this issue will not exist if we run customElements.define in ngZone.

@robwormald
Copy link
Copy Markdown
Contributor

@JiaLiPassion 's PR has landed in zone.js, so I'd appreciate people testing against that and letting us know about any changes

@JiaLiPassion
Copy link
Copy Markdown
Contributor

@robwormald, sure, I will test it!

@remackgeek
Copy link
Copy Markdown
Contributor Author

I ran my examples and everything worked as expected. Thanks, @JiaLiPassion !

Will the next version of zone.js be released with Angular 7?

@JiaLiPassion
Copy link
Copy Markdown
Contributor

@remackgeek, thanks for the confirmation, I am not sure the release date, several PRs are still pending review.

@remackgeek
Copy link
Copy Markdown
Contributor Author

@JiaLiPassion , I did some more testing and I did find an issue. Your PR doesn't seem to also fix #23841, while this one does. I have example code here. A live example is here

@JiaLiPassion
Copy link
Copy Markdown
Contributor

@remackgeek, thank you for the testing, yes, it will not work because we still need to update the customElement code here, https://github.com/angular/angular/blob/master/packages/elements/src/create-custom-element.ts#L187, to make sure the propertyChange also in ngZone.
Because zone.js not released, so we have to make this change after a new release of zone.js.

Copy link
Copy Markdown
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

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

@remackgeek, as mentioned in #37416 (comment) we have decided this is a valueable fix to get into @angular/elements. Are you still interested in working on this?

To be discussed/determined:

  • I noticed the implementation here is different than the one in remackgeek/elements-zone-strategy/.../element-zone-strategy.ts. Am I correct to assume that the implementation in this PR is more performant (when already in the zone) and covers more usecases? Is there any downside (once we address Zone being potentially undefined)?
  • If we can ensure there is no extra overhead when already inside the zone, then I would be tempted to incorporate the fix into ComponentNgElementStrategy.

constructor(componentFactory: ComponentFactory<any>, injector: Injector) {
super(componentFactory, injector);
this.ngZone = this.injector.get<NgZone>(NgZone);
this.elementZone = this.ngZone.run(() => { return Zone.current; });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This also needs to account for the possibility that Zone.js has not been loaded at all (similar to how we do in core/src/zone/ng_zone.ts#L123 and other places). Same in runInZone() below.

@remackgeek
Copy link
Copy Markdown
Contributor Author

@gkalpak, I'd be happy to work on this!

I'd guess that the PR version is more performant and handles the same cases, but I'm not a zone wizard. I shamelessly lifted the basic pattern from @JiaLiPassion in PR23885..

It would be cool to put this in ComponentNgElementStrategy, but then don't you lose the ability to tree-shake out the zone stuff for zoneless users?

@gkalpak
Copy link
Copy Markdown
Member

gkalpak commented Jun 24, 2020

don't you lose the ability to tree-shake out the zone stuff for zoneless users

Not sure 🤔 Why would that be the case? What part would prevent the tree-shaking?
Oh, are you referring to the NgZone class?

@remackgeek
Copy link
Copy Markdown
Contributor Author

I did some quick-and-dirty performance checking -- on my machine, the overhead for wrapping in ngZone.run() is about 20ms for 1 million calls. The overhead for comparing zones in the "already in zone" case is lost in the noise.
If you're not worried about tree-shaking ngZone, I'll put together a PR to modify ComponentNgElementStrategy.

@gkalpak
Copy link
Copy Markdown
Member

gkalpak commented Jun 25, 2020

I don't think NgZone would get tree-shaken anyway (due to how it is loaded), so I would go for it.

@remackgeek
Copy link
Copy Markdown
Contributor Author

The new PR is here: #37814.

@gkalpak
Copy link
Copy Markdown
Member

gkalpak commented Jun 29, 2020

Thx, @remackgeek
Closing this PR in favor of #37814.

@angular-automatic-lock-bot
Copy link
Copy Markdown

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot Bot locked and limited conversation to collaborators Jul 30, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area: elements Issues related to Angular Elements cla: yes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants