Angular 6 UI implementing change detection without using parent template - javascript

I need to make a reusable component in angular that will be packaged into a npm module. Being new to angular I've hit a roadblock with the UI change detection.
for the component to work first the application needs to make an api call to collect the data required for setup then the component needs to run some initalization.
this.dataService.getData().subscribe((data) => {
this.data = data;
this.npmComponent.init(this.data);
});
<app-npm-component></app-npm-component>
In my component I then need to run a bunch of setup tasks and afterwards it needs to listen for changes in the data rerun a sub set of setup tasks and update the ui.
#Component({
...,
changeDetection: ChangeDetectionStrategy.OnPush
});
...
init(setupData: any): void {
this.cdr.detectChanges();
this.data = setupData;
}
<ul *ngIf="data.arrayOfStuff">
<li *ngFor="let item of data.arrayOfStuff">{{item}}</li>
</ul>
the usual ChangeDection implementation throws an error: NullInjectorError: No provider for ChangeDetectorRef! since the data isn't being passed from it's parent via the template.
Whats the right way to implement this kind of thing? Thanks in advance for your time.
Edit: I found the answer in ViewChild
#ViewChild(NpmComponent, {static: false}) npmComponent: NpmComponent;

Put an input on the child
<app-npm-component [data]="setupData"></app-npm-component>
and in the child component's class
#Input() data;
Angular will take care of listening for changes to inputs.
You can also do it with subscribing using the async pipe.
In the parent component
setupData$ = this.dataService.getData();
and pass the data to the child with the async pipe
<app-npm-component [data]="setupData$ | async"></app-npm-component>
No subscriptions required.
If you need to run the data through a function use a map
setupData$ = this.dataService.getData().pipe(map(data => yourFunction(data)));

Related

What is the proper usage of asynchronoscity in an Angular app with injected service?

My goal is to implement the ability to delete a specific document in a firebase collection, as a button in a component. To do this, I'm using an Angular service to get all the document IDs in a collection, as shown below (because the IDs also have utility in other components):
getCurrUserDocID(){
this.getUsers().subscribe(arr =>{
this.listUsers = arr.map(item => {
return{
userDocID : item.payload.doc.id,
...item.payload.doc.data() as User
}
})
})
return{
userIDs : this.listUsers
};
Injecting this service into components does allow me to get the docIDs. However, it's finicky. When I called it in ngOnInit in a component that loads under a navbar tab, it doesn't work unless I click that tab a couple times:
ngOnInit(): void {
console.log(this.service.getCurrUserDocID().userIDs)
}
The first and second click return an empty array in the console, while the third finally returns the actual array of data. I suspect this has something to do with observables and asychronous-ity.
You are using a subscription to get the data from this.getUsers() observer.
On getting new data you set the value of this.listUsers.
Now all you need in your component is to access this.listUsers.
Just remember that every time there will be data coming from this.getUsers() observer the value of this.listUsers will be overwritten which will cause your component to rerender.
The fact you get the data only on the third invocation may be related to the time it takes for this.getUsers() to return date or to the way you use it.
You have to notice that the subscription to the observable will return multiple results.
Don't subscribe out of component becouse you will need to unsubscribe it. If you want to sort data use map() operator in pipe().
getCurrUserDocID(){
this.listUsers = this.getUsers().pipe(
map(item => {
return {
userDocID: item.payload.doc.id,
...item.payload.doc.data() as User
}
})
)
};
In component
constructor(public: service: Service) {} // Make it public so you can use it in html file
ngOnInit(): void {
this.service.getCurrUserDocID() // here just triger `getUsers()` function.
}
In html template with async pipe
<ul>
<li *ngFor="let item of service.listUsers | async">
{{item.id}}
</li>
</ul>
Async pipe in template are subscribing and unsubscribing for you so you no need to worry about data leaks.

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

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

pass data to different component in angular 6

I am building my web application using angular 6. I have some common component which is common on all routes. For example I have a filter component which is common on all route. Now when user select a filter a click on find this filter data should be passed to different component on same route and then result should be display. PFB my angular's app structure :
App.component.html :
<filter (messageToDash)="receiveMessage($event)"></filter>
<router-outlet></router-outlet>
For dash route I have dash component. PFB is code for dash.component.html :
<dashboard></dashboard>
Filter.component.html
<button (click)="somemethod()"></button>
So when user click on button, I want to want to pass some variable to the dashboard component. I also tried using service component and subscribe it into dashboard's ngOnInit() variable, but its not working.
Create a service and give it as reference at the parent Level and on (click) of the the button pass the filter data to the function with return type observable and in component subscribe to the result of the function.
Since ngOnInit lifecycle is initialized only at the beginning of the page load, It may not be of much help.
I too had stuck in the same problem a few months back, the best solution i got at that time was using LocalStorageService
For Example:
import { LocalStorageService } from 'ngx-webstorage';
constructor(private session: LocalStorageService)
{
//works at the begining of the module before OnInit
}
some_function()
{
this.session.store('key_name',yourData);
}
Now in another component just import LocalStorageService and create a obj for it and then:
some_function()
{
this.your_variable = this.session.retrieve('key_name');
}
Note: the key_name for storing and retrieving must be same.
hope this helps.

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);
}

Angular 2 how to pass function with params to a child component?

How do you properly pass a function from a parent to a child component when the function takes in parameters?
In the ngOnInit, how to scope a function like:
addToList(id) {
this.store.dispatch(this.listActions.addToList(id));
}
ngOnInit, which is wrong right now.
ngOnInit() {
this.addToList = this.addToList.bind(this, id);
}
In my parent component, I have the addToCart(id) function.
I want to pass that function to my child component, which has a list of items, and on clicking the ADD button on an item, I want to callback addToCart(item_id) to the parent.
#Maarek's answer is a good one, and is the 'right' way to do it, probably. What I am presenting here is a simpler means of communicating specifically from the Child to the Parent.
What you proposed in the original post was to have the Parent send a callback method to the Child, so the Child can call it with data when appropriate. To accomplish this specific task (data from Child to Parent on some action in the Child) using Events is appropriate, using the EventEmitter from inside the Child. See this API reference which has an example: https://angular.io/docs/ts/latest/api/core/index/EventEmitter-class.html and this Plunker I made as a demo: https://embed.plnkr.co/T1wFqVOhMXgX6NRfTuiC/
In the child, you have code like this:
import { Component, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'item',
template: `
<div class="item">
<button type="button" (click)="addItem()">Add</button>
<p>{{id}}
</div>
`
})
export class ItemComponent {
#Input() id: string;
//key line here: this emitter can be bound to by parent to get notifications
#Output() add: EventEmitter<string> = new EventEmitter<string>();
constructor() { }
addItem() {
//then when the button is clicked, emit events to the parent.
this.add.emit(this.id);
}
}
The Parent would call create the component like this:
<item id="1" (add)="addToList($event)"></item>
Where addToList() is a function on the Parent that does the work your callback was intended to do. The $event is the data passed from the child (the id).
There's not a lot of detail here, but from what I'm gathering I think what you will want is an injectable service (demonstrated here: https://angular.io/docs/ts/latest/tutorial/toh-pt4.html) to handle the data objects being shared between the components. Rather than type a bunch of code in here (which is better shown at that page in the tutorial) I'll describe what I think you're trying to do and how I'd go about doing it.
The entire store data model can be handled via a service (store.service.ts maybe). Which will have your CRUD functions exposed for the different properties of the store model. The list you are adding to here should have a public getter that returns an observable of the list in the service as well as a public function for adding and deleting from the list. Something like this:
#Injectable
export class StoreService {
private _storeList:BehaviorSubject<Array<any>> = new BehaviorSubject<Array<any>>([]);
/*I'm sure the store has other properties, set them up here. I'd suggest
breaking any arrays out of the general object (unless you want to use
pipes which are awesome but keeping it simple here) but if the store has
a lot of general properties (name, address, whatever) they can be stored
in a single BehaviorSubject of type any.
*/
constructor(){}
get StoreList() { return this._storeList.asObservable() }
public addToList(id) {
let curVal = this._storeList.getValue();
curVal.push(id);
this._storeList.next(curVal);
}
}
You would then inject this service into the constructor of both the parent and the child constructor(private _storeService:StoreService){} (and any other components that need it). The child could then subscribe to the list: get List() { return this._storeService.StoreList } and the parent can call the add function to add to the list. One thing to note, when you add this to your template as an *ngFor, make sure to pass the value through the async pipe. *ngFor="List | async" or your may tear your hair out trying to figure out why you're getting errors.
This article helped me a lot with this as well (although I might suggest avoiding immutable at first until you're comfortable with Angular 2 completely): http://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/

Categories