How do I dynamically create components with third party library in Angular? - javascript

Using Angular 10
There are many questions on SO that are similar to this, but I have yet to find one that answers my situation.
I'm hoping someone can guide me.
I'm using a third party library to display 360° photos. This third party library has a built-in API to display hotspots in the scene. Simply give the library the element you want to be the hotspot, and it takes care of the rest.
I have most of it working as expected, but there are a couple pieces that are not.
So far, I'm dynamically generating my components like so:
this._hotspotFactory = this.resolver.resolveComponentFactory(HotspotComponent);
const component = this._hotspotFactory.create(this.injector);
//Hydrate component with bunch of data
component.instance.id = data.id;
...
// Create the Hotspot with Third Party
// Calling this third party method injects the native element into the DOM.
// Passing the nativeElement in. Looks great at first glance.
const hotspot = this._scene.createHotspot(data, component.location.nativeElement);
this.appRef.attachView(component.hostView);
component.hostView.detectChanges();
if(component.instance.over.observers.length) {
hotspot.on('over', (evt) => {
this.zone.run(() => {
component.instance.over.emit(evt);
});
});
}
if(component.instance.out.observers.length) {
hotspot.on('out', (evt) => {
this.zone.run(() => {
component.instance.out.emit(evt);
});
});
}
if(component.instance.navigate.observers.length) {
hotspot.on('click', (evt) => {
this.zone.run(() => {
component.instance.navigate.emit(evt);
})
});
}
No errors are thrown and I successfully see the hotspot where it should be in the scene. Even Data interpolation in the HotspotComponent template occurs as expected.
BUT, [ngStyle] bindings never result in dynamic styling in HotspotComponent.
I'm 99% sure this is because change detection is not taking place in the component.
I am manually attaching the view with this.appRef.attachView(component.hostView) because the third party is responsible for injecting the element into the DOM, not Angular. Thus Angular needs to know about it so it will perform change detection.
Even with manually calling attachView, I still think Angular doesn't know about this component in the view because the Angular Chrome Extension debugger doesn't register it in its dev tools as a known component in the view....despite be seeing it on screen and in the DOM.
What am I missing?

What change detection strategy does the component have?
When a component is added to a view, it's life cycle hooks will be triggered by angular(ngOninit, ngAfterContentInit etc). Log something in these and see if theme life cycle hooks are being called. Irrespective of the change detection strategy one change detection cycle should happen on the component after it is added to view.
If the life cycle hook invoking is not happening, then it would mean that angular is not involved in adding the element to DOM.

It seems angular has a lifecycle hook precisely for your use-case 'ngDoBootstrap'.
As we can not debug your full source code, from the information you have mentioned it seems the dynamic component you are trying to attach to the view is not available to Angular in NgModule. Every component that angular bootstraps must be in NgModule.
you can although bootstrap it dynamically using 'ngDoBootstrap'.
It is used in the following manner:
ngDoBootstrap(appRef: ApplicationRef) {
this.fetchDataFromApi().then((componentName: string) => {
if (componentName === 'ComponentOne') {
appRef.bootstrap(ComponentOne);
} else {
appRef.bootstrap(ComponentTwo);
}
});
}
In your case, you can do it before attaching the component to the view.
...
appRef.bootstrap(component);
this.appRef.attachView(component.hostView);
component.hostView.detectChanges();
...
Please check the documentation here: https://angular.io/api/core/ApplicationRef

We use resolveComponentFactory method given by ComponentFactoryResolver class present in angular which is used for component level lazy loading. For the confirmation that your component is really breaked in chunks, do ng build --prod or you con and you will see the generated .js chunk for SoftwareListComponent.
app.component.html
<button (click)="loadSoftwareListDynamically()> Load </button>
<div #softwareListContainer></div>
app.component.ts
constructor(
private componentFactoryResolver: ComponentFactoryResolver
) {}
#ViewChild('softwareListContainer', { read: ViewContainerRef })
softwareListContainer: ViewContainerRef;
loadSoftwareListDynamically() {
import('../common-features/software-list/software-list.component').then(
({ SoftwareListComponent }) => {
const componentFactory =
this.componentFactoryResolver.resolveComponentFactory(
SoftwareListComponent
);
this.softwareListContainer.createComponent(componentFactory);
}
);
}
software-list.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
#NgModule({
declarations: [
SoftwareListComponent
],
imports: [
CommonModule,
RouterModule,
SwiperModule <- External Library
],
})
export class SoftwareListModule {}
For more info, you can go over to my complete discussion for Lazy loading of components created using ComponentFactoryResolver. You will get more info here ->
StackOverflow Discussion
Stackblitz Link Here

Related

Calling an angular component method from classic HTML

I'm trying to call a function defined within an angular component.ts from another javascript library
The library supports a snapin, where I can define HTML Elements, which will be rendered (they are not part of angular).
Here is my html (not in a component's template)
<button onclick='ping()'>PING Button</button>
<button onclick='pong()'>PONG Button</button>
How can I, from the html above, call the pong component method defined my component.ts
import { Component, OnInit, AfterViewInit, Inject, ChangeDetectionStrategy, ChangeDetectorRef } from '#angular/core';
function ping() {
alert('PING PING');
}
#Component({ ...
})
export class Component implements OnInit, AfterViewInit { ...
pong() { //This is the method I want to call from outside of the component's template
alert('pong pong');
}
}
I tried, but it will not work
<button (click)="pong()">PONG Button</button>
But I have no idea to call the "pong()" function normally
Thanks!
If you really need this, you can make the method available in the window object
component.ts
constructor()
{
window['pong'] = () => this.pong();
}
pong()
{
alert ('pong inside component')
}
Then in your html, you can use old style event handler
<button onclick="pong()">Pong</button>
Here is a stackblitz demo
Note: If you have several instance of the same angular component implementing this solution, you'll only have once instance of the method. You could save them all to an array if needed, but you'll need to know which one to call
I guess you want to use a non-angular library from angular, which has a global callback. Be warned that this can lead to problems, because you have to manage the lifecycle of the non-angular thing from angular.
From the angular template, you can only call methods on the component class, and you can't call a global callback. You can however create a method on the component class, and call the global callback from there.
There's one more thing before that's possible: typescript doesn't know about your global callback, so you have to explictily declare it, see the example. This tells typescript that there's something that is created outside of typescript, so it will let you call it.
import { Component } from '#angular/core';
declare const libMethod: (any) => any;
#Component({
selector: 'my-app',
template: `
<button (click)="myMethod($event)"></button>
`,
styleUrls: []
})
export class AppComponent {
public myMethod(param) {
libMethod(param);
}
}
If you plan to use that library from multiple of your angular components, then you might want to create a service, declare the global callback only in that, and create a method on the service. That way, this somewhat hacky declaration will not be littered all over your code, but contained to a single place. It also makes your life easier, if you upgrade/replace this library.
Answers to questions in the comments:
TBH I don't completely understand the situation. (Who calls the backend, when it returns the HTML? You from Angular, or the lib? Does the lib process the HTML? Or what does it do?)
Some suggestions: create a global singleton service, which puts up one of its methods to the window (dont' forget to bind it if you use this inside the method) to serve as a callback for the lib. When the lib calls it with the data, regardless of who/when actually triggered the lib to do its thing, the service stores the data in a subject, and the service also provides an observable of the data (maybe with a shareReplay(1) so that the consumers always get something).
With that, actually displaying the data is fairly easy, you can just use the async pipe, and not care about how/when the data got there in the first place, and don't have to sync the component's lifecycle with the service.
Also, you probably need to use https://angular.io/api/platform-browser/DomSanitizer#bypasssecuritytrusthtml but I am not sure about that, since I never had to inject HTML. Speaking about which...
Important security notice: if you inject HTML from outside of angular, and that is hijacked, you just opened up your page to all kind of nasty cross site scripting things.

Angular 2 - How to dynamically change an entire CSS stylesheet based on URL queryparams

I'm very new to the world of Angular (loving it so far).
We have an Angular 1 (JS) app that we plan to convert to the latest Angular 2 (8.3.9) version.
One thing that was done in the old app, was make use of the $scope object, and then set the CSS stylesheet link in the root index.html dynamically based on a query parameter in the requesting URL.
Using ngStyle or ngClass to update indiviudal elements in a document it cool, but,
How do we handle changing the entire style sheets on loading the app in Angular 2?
Everything is encapsulated inside the component, and styles specified in the angular.json file are built into the "deployable.js" file. Is it possible to change it at runtime?
This link appears to be the closest example:
Generate dynamic css based on variables angular
, but still doesn't quite solve the problem at the root index.html file.
The Current OLD Version
Url Arrives:
http://someserver:port/app?css=http://linktoCSs/cssFile.css
css query param is pulled into a global variable and stored on scope called css_url,
In index.html (starting page)
<link rel="stylesheet" ng-href="{{css_url}}">
and the app starts using whatever style sheet this link provides.
The NEW Angular 2 (version 8.3.9)
We want to mimic the above behavior that we were able to achieve in the JS version. Pull a URL from a QueryParam pointing to some CSS file, and that CSS URL can be injected into the main index.html to change the entire apps styles in one go, or dynamically access the style-sheet in code to update everything in one go.
In short, we want 1 app that can be styled by a CSS file, based off a queryparam.
All thoughts and comments will be greatly appreciated.
After a lot of digging around, finally found the solution I was looking for.
And it was so straight forward, hope this helps others that might be needing to do the same thing..
Get the css path from query params and then Inject the Document into a TS file...
append the link to the head of the document.
I did this in a Router Guard using canActivate.
I got the query param from the routerstatesnpshot like so:
Inject the DOCUMENT( don't forget the import) in the constructor
http://server.com/xxx&broker=1&client=2&css=http://cssServer.com/my_dynamic_stylesheet.css
import { DOCUMENT } from '#angular/common';
#Inject(DOCUMENT) private document: Document
this.setDynamicStyle(state.root.queryParamMap.get('css'));
setDynamicStyle(cssURL: string) {
const head = this.document.getElementsByTagName('head')[0];
const style = this.document.createElement('link');
style.id = 'css-styling';
style.rel = 'stylesheet';
style.href = `${cssURL}`;
head.appendChild(style);
}
Thanks to:
https://juristr.com/blog/2019/08/dynamically-load-css-angular-cli/
This appears to be a fairly common issue, updating styling dynamically, and I can't find anything where people have attempted to override the entire CSS file/s for an Anuglar 2 app, post compilation.
What I ended up doing (for now)...
On GitHub link: https://github.com/angular/angular/issues/9343
Thanks to: peteboere
Created a directive like
import {Directive, ElementRef, Input} from '#angular/core';
#Directive({
selector: '[cssProps]',
})
export class CSSPropsDirective {
#Input() cssProps: any;
constructor(private element: ElementRef) {}
ngOnChanges({cssProps})
{
if (cssProps && cssProps.currentValue)
{
const {style} = this.element.nativeElement;
for (const [k, v] of Object.entries(cssProps.currentValue))
{
style.setProperty(k, v);
}
}
}
}
Which unfortunately means placing the cssProps on EACH and EVERY element in my documents that needs styling... like:
<div class="panel-heading" [cssProps]="cssStyling">
Then I created a simple Service to fetch styling in a JSON format like,
{--backgroundColor: "black"}
in the css file per component I used custom css properties, to handle these.
background: var(--backgroundColor)
--backgroundColor: #008000;
NOTE TO SELF, doing this at app-root level, might mean not having to apply styles per component css file..., but we will still need to apply the directive to each element we want to style dynamically. ViewEncapsulation will need to be NONE across lower components.
Then, on an httpClient call every 10 seconds (for now) like:
import { HttpClient } from '#angular/common/http';
import { Subject, Subscription } from 'rxjs';
import { Injectable } from '#angular/core';
#Injectable({providedIn: 'root'})
export class StyleSerivce {
styleChanged = new Subject<any>();
interval = new Subscription();
styles:any;
constructor(private httpClient: HttpClient) { }
retrieveStyleFrom() {
setInterval(() => {
this.httpClient.get('https://serverName/stylesss.json')
.subscribe(data => {
console.log(data);
this.styles = data;
this.styleChanged.next(this.styles);
})
}, 10000);
}
}
I subscribed to the styleChange in the respective components like:
this.styleSub = this.styleService.styleChanged.subscribe((styleNow) => {
this.cssStyling = styleNow;
})
and with cssStyling bound to my directive, as I update the DB where the JSON is stored,
I can update the screen dynamically... (after the 10 second elapse)
This works, but the pain of setting this on each tag to make it dynamically updatable from DB/JSON updates is hectic.
I would not call this a solution to the initial problem, but a painful workaround.

How to create a widget with react for integration in other react app?

I would like to know if it is possible to write a widget with React and distribute it as a CDN to integrate it into another application react?
The idea:
I have several applications in writing with react and I would like to have a banner common to all applications without having to rewrite it in each of them. The goal is to facilitate the updates of this banner.
My widget named toolBar works perfectly when I do npm start.
I have build my toolBar and add the script generate in the folder build into an other app named myAppTest.
My toolBar work and the other app to. However, the css and toolBar images are not loaded properly when launching myAppTest.
What is the best way pleasure ?
If by "integrating a widget with a react application" you mean a stand-alone react application (your widget) that needs to get data and pass data back to another application (react application in your case) then it's totally doable.
I even posted an article with a tutorial on how to do just that.
Basically the gist of it is that most of us learned that each react application has an entry point that run this line:
ReactDOM.render(<App/>, myContainer);
As in fire and forget approach.
But actually we can run ReactDOM.render as much as we want, it won't re-mount our application but instead will trigger the diffing for the tree.
If the React element was previously rendered into container, this will perform an update on it and only mutate the DOM as necessary to reflect the latest React element.
So what we can do is wrap it in a function and expose it globally so other code on the page can run it.
A pattern i use is to accept props with that function and it will pass it on to the <App/> via ReactDOM.render.
For example:
window.CoolWidget = {
mount: (props, container) => {
ReactDOM.render(<CoolWidget {...props} />, container);
},
unmount: (container) => {
ReactDOM.unmountComponentAtNode(container);
}
}
And the consumers of your widget can run it:
window.CoolWidget.mount({widgetProp: someValue, onLogin: function(){...}}, myContainer)
The best thing with this approach is that they can convert this code to a react component (or maybe even you can do it for them!):
class CoolWidgetWrapper extends PureComponent {
// create a ref so we can pass the element to mount and unmount
widgetRef = React.createRef();
componentDidMount() {
// initial render with props
window.CoolWidget.mount(this.props, this.widgetRef.current);
}
componentDidUpdate(prevProps) {
if(prevProps !== this.props){
window.CoolWidget.mount(this.props, this.widgetRef.current);
}
}
componentWillUnmount(){
window.CoolWidget.unmount(this.widgetRef.current);
}
render() {
return <div ref={this.widgetRef}></div>
}
}
And this is how they will use it:
<CoolWidgetWrapper someProp={someValue} onLogin={someFunc} />
Hope that helps

Angular - dynamically destroying a component, *ngIf and ComponentRef

I'm making a pop-up component that I want to use in several of my other components, so I made a popup.service that enable the component to be loaded through *ngIf inside other components. This is creating a problem for me since the PopupComponent is a separate entity and I'm unsure how to pass data from the child component(PopupComponent) to its respective parents.
Atm the loading looks like this in ParentComponent.ts:
public openPopup(order_id: string, invoice_id: string): void{
this.load_popup=this.popupService.openPopup(order_id, "selected_order", invoice_id, "selected_invoice");
}
And ParentComponent.html:
<app-popup *ngIf="load_popup"></app-popup>
And it loads like a charm, the problem is in closing it. The close button is located on the PopupComponent, is there an efficient way to have the Child Component (PopupComponent) to affect a variable in the Parent Component ie. ParentComponent.load_popup=false?
My other thought was dynamically loading the component, however I have no idea on how to do that. I was fidgeting around with using the PopupService and putting something like this in it:
import { Injectable, ComponentRef } from '#angular/core';
import {PopupComponent} from '../popup/popup.component';
#Injectable({
providedIn: 'root'
})
export class PopupService {
popup_ref: ComponentRef<PopupComponent>
constructor(
) { }
//Implemented in orderoverviewcomponent, invoicecomponent, and placeordercomponent
public openPopup(id1:string, storage_label1:string, id2:string, storage_label2:string): Boolean{
if (id1){
localStorage.setItem(storage_label1, JSON.stringify(id1));
}
if (id2){
localStorage.setItem(storage_label2, JSON.stringify(id2));
}
this.popup_ref.initiate(); //this line is a made up example of loading the component
return true;
}
public closePopup(storage_label1: string, storage_label2:string): Boolean{
if(storage_label1){
localStorage.removeItem(storage_label1);
}
if(storage_label2){
localStorage.removeItem(storage_label2);
}
this.popup_ref.destroy();
return false;
}
}
Where this.popup_ref.destroy(); would ideally destroy PopupComponent, but when I did that I got a "cannot read property of undefined" on the popup_ref, I'm having trouble declaring it, the syntax seems a bit tricky.
The problem also remains that i need a function to load the component, the opposite of .destroy(), if this is possible I would much prefer it over loading and destroying with *ngIf.
Edit: Partially solved it by just using a boolean in the service as the trigger for *ngIf, is there a way to do a function load and destroy on a component still?
You can bind an EventEmitter() to your component to invoke a function in the parent component.
<app-popup [onClose]="load_popup = false" *ngIf="load_popup"></app-popup>
Then inside of your app-popup component:
#Output onClose = new EventEmitter();
public closePopup(/* code emitted for brevity */) {
/* code emitted for brevity */
this.onClose.emit(); //Call the parent function (in this case: 'load_popup = false')
}
It's important to know that you can pass entire functions to the bound function, and you can even pass variables back to the parent from the child:
[onClose]="myFunction($event)"
this.onClose.emit(DATA HERE);
As an aside, since you're using Angular; I would suggest looking into using Modals for popup dialogue boxes. You can see a good example here:
https://ng-bootstrap.github.io/#/components/modal/examples

Angular 4 - "Expression has changed after it was checked" error while using NG-IF

I setup a service to keep track of logged in users. That service returns an Observable and all components that subscribe to it are notified (so far only a single component subscribe to it).
Service:
private subject = new Subject<any>();
sendMessage(message: boolean) {
this.subject.next( message );
}
getMessage(): Observable<any> {
return this.subject.asObservable();
}
Root App Component: (this component subscribes to the observable)
ngAfterViewInit(){
this.subscription = this._authService.getMessage().subscribe(message => { this.user = message; });
}
Welcome Component:
ngOnInit() {
const checkStatus = this._authService.checkUserStatus();
this._authService.sendMessage(checkStatus);
}
App Component Html: (this is where the error occurs)
<div *ngIf="user"><div>
What I'm trying to do:
I want every component (except the Root App Component) to send the users logged-in state to the Root App Component so I can manipulate the UI within the Root App Component Html.
The issue:
I get the following error when the Welcome Component is initialised.
Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'true'.
Please note this error occurs on this *ngIf="user" expression which is located within Root App Components HTML file.
Can someone explain the reason for this error and how I can fix this?
On a side note: If you think theres a better way to achieve what I'm trying to do then please let me know.
Update 1:
Putting the following in the constructor solves the issue but don't want to use the constructor for this purpose so it seems it's not a good solution.
Welcome Component:
constructor(private _authService: AuthenticationService) {
const checkStatus = this._authService.checkUserStatus();
this._authService.sendMessage(checkStatus);
}
Root App Component:
constructor(private _authService: AuthenticationService){
this.subscription = this._authService.getMessage().subscribe(message => { this.usr = message; });
}
Update 2:
Here's the plunkr. To see the error check the browser console. When the app loads a boolean value of true should be displayed but I get the error in the console.
Please note that this plunkr is a very basic version of my main app. As the app is bit large I couldn't upload all the code. But the plunkr demonstrates the error perfectly.
What this means is that the change detection cycle itself seems to have caused a change, which may have been accidental (ie the change detection cycle caused it somehow) or intentional. If you do change something in a change detection cycle on purpose, then this should retrigger a new round of change detection, which is not happening here. This error will be suppressed in prod mode, but means you have issues in your code and cause mysterious issues.
In this case, the specific issue is that you're changing something in a child's change detection cycle which affects the parent, and this will not retrigger the parent's change detection even though asynchronous triggers like observables usually do. The reason it doesn't retrigger the parent's cycle is becasue this violates unidirectional data flow, and could create a situation where a child retriggers a parent change detection cycle, which then retriggers the child, and then the parent again and so on, and causes an infinite change detection loop in your app.
It might sound like I'm saying that a child can't send messages to a parent component, but this is not the case, the issue is that a child can't send a message to a parent during a change detection cycle (such as life cycle hooks), it needs to happen outside, as in in response to a user event.
The best solution here is to stop violating unidirectional data flow by creating a new component that is not a parent of the component causing the update so that an infinite change detection loop cannot be created. This is demonstrated in the plunkr below.
New app.component with child added:
<div class="col-sm-8 col-sm-offset-2">
<app-message></app-message>
<router-outlet></router-outlet>
</div>
message component:
#Component({
moduleId: module.id,
selector: 'app-message',
templateUrl: 'message.component.html'
})
export class MessageComponent implements OnInit {
message$: Observable<any>;
constructor(private messageService: MessageService) {
}
ngOnInit(){
this.message$ = this.messageService.message$;
}
}
template:
<div *ngIf="message$ | async as message" class="alert alert-success">{{message}}</div>
slightly modified message service (just a slightly cleaner structure):
#Injectable()
export class MessageService {
private subject = new Subject<any>();
message$: Observable<any> = this.subject.asObservable();
sendMessage(message: string) {
console.log('send message');
this.subject.next(message);
}
clearMessage() {
this.subject.next();
}
}
This has more benefits than just letting change detection work properly with no risk of creating infinite loops. It also makes your code more modular and isolates responsibility better.
https://plnkr.co/edit/4Th7m0Liovfgd1Z3ECWh?p=preview
Declare this line in constructor
private cd: ChangeDetectorRef
after that in ngAfterviewInit call like this
ngAfterViewInit() {
// it must be last line
this.cd.detectChanges();
}
it will resolve your issue because DOM element boolean value doesnt get change. so its throw exception
Your Plunkr Answer Here Please check with AppComponent
import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
import { MessageService } from './_services/index';
#Component({
moduleId: module.id,
selector: 'app',
templateUrl: 'app.component.html'
})
export class AppComponent implements OnDestroy, OnInit {
message: any = false;
subscription: Subscription;
constructor(private messageService: MessageService,private cd: ChangeDetectorRef) {
// subscribe to home component messages
//this.subscription = this.messageService.getMessage().subscribe(message => { this.message = message; });
}
ngOnInit(){
this.subscription = this.messageService.getMessage().subscribe(message =>{
this.message = message
console.log(this.message);
this.cd.detectChanges();
});
}
ngOnDestroy() {
// unsubscribe to ensure no memory leaks
this.subscription.unsubscribe();
}
}
Nice question, so, what causes the problem? What's the reason for this error? We need to understand how Angular change detection works, I'm gonna explain briefly:
You bind a property to a component
You run an application
An event occurs (timeouts, ajax calls, DOM events, ...)
The bound property is changed as an effect of the event
Angular also listens to the event and runs a Change Detection Round
Angular updates the view
Angular calls the lifecycle hooks ngOnInit, ngOnChanges and ngDoCheck
Angular run a Change Detection Round in all the children components
Angular calls the lifecycle hooks ngAfterViewInit
But what if a lifecycle hook contains a code that changes the property again, and a Change Detection Round isn't run? Or what if a lifecycle hook contains a code that causes another Change Detection Round and the code enters into a loop? This is a dangerous eventuality and Angular prevents it paying attention to the property to don't change in the while or immediately after. This is achieved performing a second Change Detection Round after the first, to be sure that nothing is changed. Pay attention: this happens only in development mode.
If you trigger two events at the same time (or in a very small time frame), Angular will fire two Change Detection Cycles at the same time and there are no problems in this case, because Angular since both the events trigger a Change Detection Round and Angular is intelligent enough to understand what's happening.
But not all the events cause a Change Detection Round, and yours is an example: an Observable does not trigger the change detection strategy.
What you have to do is to awake Angular triggering a round of change detection. You can use an EventEmitter, a timeout, whatever causes an event.
My favorite solution is using window.setTimeout:
this.subscription = this._authService.getMessage().subscribe(message => window.setTimeout(() => this.usr = message, 0));
This solves the problem.
To understand the error, read:
Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error
You case falls under the Synchronous event broadcasting category:
This pattern is illustrated by this plunker. The application is
designed to have a child component emitting an event and a parent
component listening to this event. The event causes some of the parent
properties to be updated. And these properties are used as input
binding for the child component. This is also an indirect parent
property update.
In your case the parent component property that is updated is user and this property is used as input binding to *ngIf="user". The problem is that you're triggering an event this._authService.sendMessage(checkStatus) as part of change detection cycle because you're doing it from lifecycle hook.
As explained in the article you have two general approaches to working around this error:
Asynchronous update - this allows triggering an event outside of change detection process
Forcing change detection - this adds additional change detection run between the current run and the verification stage
First you have to answer the question if there's any need to trigger the even from the lifecycle hook. If you have all the information you need for the even in the component constructor I don't think that's the bad option. See The essential difference between Constructor and ngOnInit in Angular for more details.
In your case I would probably go with either asynchronous event triggering instead of manual change detection to avoid redundant change detection cycles:
ngOnInit() {
const checkStatus = this._authService.checkUserStatus();
Promise.resolve(null).then(() => this._authService.sendMessage(checkStatus););
}
or with asynchronous event processing inside the AppComponent:
ngAfterViewInit(){
this.subscription = this._authService.getMessage().subscribe(Promise.resolve(null).then((value) => this.user = message));
The approach I've shown above is used by ngModel in the implementation.
But I'm also wondering how come this._authService.checkUserStatus() is synchronous?
I recently encountered the same issue after migration to Angular 4.x, a quick solution is to wrap each part of the code which causes the ChangeDetection in setTimeout(() => {}, 0) // notice the 0 it's intentional.
This way it will push the emit AFTER the life-cycle hook therefore not cause change detection error.
While I am aware this is a pretty dirty solution it's a viable quickfix.
Don't change the var in ngOnInit, change it in constructor
constructor(private apiService: ApiService) {
this.apiService.navbarVisible(false);
}

Categories