@angular/elements + zone.js change detection related to RxJs stream runs in wrong zone
See original GitHub issueReproduction
You can get running example of the last solution using this one liner git clone https://github.com/tomastrajan/angular-elements-cd-example.git && cd angular-elements-cd-example && npm i && npm start
This will be working, it can be broken by removing { ngZone: (window as any).ngZone }
from elements main.ts
file (as per description)
Description
Basically it’s about using @angular/elements
inside of the Angular application. It all started with weird behavior in change detection but ONLY in rxjs
observable streams…
The stream in element did not trigger change detection…
Example, we have an @Input() username
and we display it inside of element but also use it to load repositories from github API, the change to username will:
- trigger component re-render ( and display actual username in template )
- trigger
ngOnChanges
- trigger backend request as a part of observable stream ( also receive data)
but the change will NOT:
- trigger re-render as a result of received data from backend
Causes
It seems to be the case when current zone is logged out inside of the rxjs
steam it is the parent Angular app zone instead of the element zone…
Explored solution 1: zone.js/dist/zone-patch-rxjs
I tried to solve it in various ways, the most promising was to add import 'zone.js/dist/zone-patch-rxjs';
to the element BUT…
This leads to second more serious problem …
If the consumer SPA or ( any of it libs ) already uses import 'zone.js/dist/zone-patch-rxjs';
then any subsequent call of import 'zone.js/dist/zone-patch-rxjs';
which calls Zone.__load_patch('rxjs', handler ...)
will be ignored because in zone.js
there is
static __load_patch(name, fn) {
if (patches.hasOwnProperty(name)) { // only first rxjs patch will be applied
if (checkDuplicate) {
throw Error('Already loaded patch: ' + name); // this will not happen because checkDuplicate = false by default
}
}
else if (!global['__Zone_disable_' + name]) {
const perfName = 'Zone:' + name;
mark(perfName);
patches[name] = fn(global, Zone, _api); // patch rxjs
performanceMeasure(perfName, perfName);
}
}
so then if element comes with it’s own rxjs
in its bundle and tries to import 'zone.js/dist/zone-patch-rxjs';
patch it, it will be ignored because zone already has rxjs
patch…
Explored solution 2: pass parent Angualr app NgZone
into the element
In terms of solutions what we do now is
In element:
- do not import any zone or any patch
- use
.bootstrapModule(AppModule, { ngZone: (window as any).ngZone })
In consumer SPA: 1.
export class AppModule {
constructor(private ngZone: NgZone) {
(window as any).ngZone = this.ngZone // store reference on window to be used by element during its bootstrap
}
}
The solution seems pretty dirty, would be curious about why we can’t patch more than one rxjs
and in what direction will the element go and if anybody ever struggled with this ?
Reproduction (copy)
You can get running example of the last solution using this one liner git clone https://github.com/tomastrajan/angular-elements-cd-example.git && cd angular-elements-cd-example && npm i && npm start
The error behavior (when rxjs does NOT trigger change detection after receiving of data) we can simply remove { ngZone: (window as any).ngZone }
from the element main.ts
file
Questions
- why is rxjs broken at all ? (everything else seems to be working)
- why we’re not allowed to
zone-patch-rxjs
for multiplerxjs
bundles in the single page ? - which solution makes most sense based on the roadmap of Angular and elements?
- what can go wrong when sharing parent ANgular
NgZone
instance with the child element? - are the other solutions ( which does NOT involve manual CD because we need to convert existing Angular apps into elements without major rewrite)?
cc: @JiaLiPassion @robwormald @manfredsteyer
Versions
Angular / CLI / elements 8
Issue Analytics
- State:
- Created 4 years ago
- Reactions:9
- Comments:13 (12 by maintainers)
Top GitHub Comments
Hey @JiaLiPassion , I debugged it further and it turned out to be something else, basically a stream was subscribed in
runOutsideAngular
and this made then any subsequent subscription to a other stream which was part of the first one to also run in<root>
zone.The solution was to NOT subscribe in
runOutsideAngular
so that the sub-stream is not pulled out and then everything works as expected.@tomastrajan, thank you very much for creating the documentation, and I will try to find out an elegant solution for this issue. Have a nice day 😄