Angular ngOnChanges in child doesn't trigger after subscribe manipulates data - javascript

I have a problem with ngOnChanges not being fired for a child component after data is being manipulated in parent via subscribe method.
Basically my main component looks like this:
public imageGroups: IImageGroup[];
public status$: Observable<ModelStatusesModel>;
public ngOnChanges(changes: SimpleChanges): void {
if (Boolean(this.treeData)) {
this.imageGroups = this.treeDataService.convertImageGroups(this.treeData);
this.status$ = this.updateStatus();
this.status$.subscribe((status: ModelStatusesModel) => {
this.treeDataService.updateStatus(this.imageGroups, status); // this function makes changes to this.imageGroups
console.log('subscribe happened');
});
}
}
HTML:
...
<ul class="treeview-nodes-wrapper">
<treeview-branch *ngFor="let group of (imageGroups)"
[group]="group"></treeview-branch>
</ul>
...
The branch has also ngOnChnages:
public ngOnChanges(): void {
this._currentNodeDisabled = this.group.isDisabled;
console.log(this.group); //returns isDisables as true
console.log(this.group.isDisabled); //returns false
console.log(this._currentNodeDisabled); //returns false
}
When I run my application this is the result in the console:
{ ... isDisabled: true ...},
false
false
subscribe happened
I was also trying to surround the call inside my subscription in a ngZone.run but without any success. Do you have any idea how to tell angular that ngOnChanges in the child triggered?
EDIT: What works for me is creating a property in the parent component (public change = false;) then toggling this property inside my subscribe method and giving it as an input to my children elements. That gets picked up as a change. Even though this solves my problem, it looks more like a very hacky way of writing the code.

This is a result of ngOnChanges being triggered when an input value changes, and with objects that are passed by reference the modification of their properties does not explicitly change the thing being passed down. The solution is often times to do a clone, ...spread operator, or reassign to the property that is being passed down in the parent's [input].
Your solution of having an additional input that changes to a new value to trigger ngOnChanges works as well, it's a bit of a workaround but that's web dev. just remember if you set the property to true then true again it won't trigger, so a count variable may work better (it's kinda hacky).

Doing a clone via JSON.parse solved my problem in a cleaner way than toggling a variable :
...
this.status$.subscribe((status: ModelStatusesModel) => {
this.treeDataService.updateStatus(this.imageGroups, status);
triggerChange();
});
...
private triggerChange() {
this.imageGroups = JSON.parse(JSON.stringify(this.imageGroups));
}

Related

Why ngOnChanges() does not trigger when #Input() update the data Angular 8?

parent.component.html
<parent-comp [data]="mydata"> </parent-comp>
parent.component.ts
this.service.abc$
.takeUntil(this.ngUnsubscribe.asObservable())
.subscribe((data: myType[]) => {
this.mydata= data;
});
child.component.ts
#Input data;
Under Class I have below code
public ngOnChanges() { if (this.data) { console.log(this.data); } }
Now I want whenever I receive latest data in #Input data from Parent Component to child then my ngOnChanges function should trigger and print data in console.
But unfortunately ngOnChanges function does not trigger again. It trigger only once when component initialize
Please let me know if anyone wants more detail on same!
Thanks!
Given the lack of further information, I'd make an informed guess that #Input data is either an array or an object.
According to docs, ngOnChanges is:
A lifecycle hook that is called when any data-bound property of a
directive changes.
What it doesn't say however is how the property should be changed. Generally speaking, the hook is only triggered when the reference to the property is changed.
Consider the following eg.
Parent component controller
mydata = [];
updateMyData(value: any) {
this.mydata.push(value);
}
Parent component template
<app-child [data]="mydata"></app-child>
Child component controller
#Input() data: any;
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
}
Now you'd expect the ngOnChanges will be triggered every time the updateMyData() function is called in the parent component. But the reference to the variable mydata is never changed. So the hook won't be triggered. There are multiple ways to force the change detector to trigger the hook.
Method 1:
Bind the #Input decorator to a setter instead of the variable directly. Discussed in an answer already.
Method 2:
Use spread syntax to re-assign the variable in the parent component.
Parent component controller
mydata = [];
updateMyData(value: any) {
this.mydata = [...this.mydata, value];
}
You could use the same methods for objects as well.
Thank you everyone for your quick and effective solutions.
I got solution and it is not exactly but similar to what you guys suggested.
In the Parent Component:
**Earlier I was assigning this way**
`this.mydata= data;`
**But now I am assigning in below way:**
`this.mydata= cloneDeep(data);`
Note : cloneDeep is imported from lodash
It could be the data you are passing down. If it doesn't change then the ngOnChanges won't register any changes. Here's an example, you can see if a property changes multiple times then it will only trigger on the first update, but if you recreate the object it changes every time.
(see console logs in stackblitz)
https://stackblitz.com/edit/angular-ivy-qmb35h?file=src%2Fapp%2Fapp.component.html
You can do as I did and recreate the object each time to bypass this, or a more hacky way may be to keep a dummy 'count' variable that you pass down as well, and increment it each time you want the child component to register the change.
NgOnChanges will only be triggered for an input-bound property change of primitive type. That is because the reference to the data-variable has to be changed in order for the change to be registered, so that you can get it in this life-cycle hook.
So, the possible way you could achieve this is by changing the reference of 'mydata' variable. Like, assigning a new reference to the mydata variable when it is changed, mydata = [...mydata] if it is an array, or mydata = {...mydata} if it is an object from the parent component.
you can use setter and getter methods for #Input in angular.
Please refer the below lines for reference:
private _data: any;
#Input()
set data(data) {
this._data = data;
console.log(this._data);
};
From setter method only you can call any other method as well and can run any logic you want from there.

Changing ContentChildren models on QueryList.changes

Suppose I have a parent component with #ContentChildren(Child) children. Suppose that each Child has an index field within its component class. I'd like to keep these index fields up-to-date when the parent's children change, doing something as follows:
this.children.changes.subscribe(() => {
this.children.forEach((child, index) => {
child.index = index;
})
});
However, when I attempt to do this, I get an "ExpressionChangedAfter..." error, I guess due to the fact that this index update is occurring outside of a change cycle. Here's a stackblitz demonstrating this error: https://stackblitz.com/edit/angular-brjjrl.
How can I work around this? One obvious way is to simply bind the index in the template. A second obvious way is to just call detectChanges() for each child when you update its index. Suppose I can't do either of these approaches, is there another approach?
As stated, the error comes from the value changing after the change cycle has evaluated <div>{{index}}</div>.
More specifically, the view is using your local component variable index to assign 0... which is then changed as a new item is pushed to the array... your subscription sets the true index for the previous item only after, it has been created and added to the DOM with an index value of 0.
The setTimout or .pipe(delay(0)) (these are essentially the same thing) work because it keeps the change linked to the change cycle that this.model.push({}) occurred in... where without it, the change cycle is already complete, and the 0 from the previous cycle is changed on the new/next cycle when the button is clicked.
Set a duration of 500 ms to the setTimeout approach and you will see what it is truly doing.
ngAfterContentInit() {
this.foos.changes.pipe(delay(0)).subscribe(() => {
this.foos.forEach((foo, index) => {
setTimeout(() => {
foo.index = index;
}, 500)
});
});
}
It does indeed allow the value to be set after the element is rendered on
the DOM while avoiding the error however, you will not have the value
available in the component during the constructor or ngOnInit if
you need it.
The following in FooComponent will always result in 0 with the setTimeout solution.
ngOnInit(){
console.log(this.index)
}
Passing the index as an input like below, will make the value
available during the constructor or ngOnInit of FooComponent
You mention not wanting to bind to the index in the template, but it unfortunately would be the only way to pass the index value prior to the element being rendered on the DOM with a default value of 0 in your example.
You can accept an input for the index inside of the FooComponent
export class FooComponent {
// index: number = 0;
#Input('index') _index:number;
Then pass the index from your loop to the input
<foo *ngFor="let foo of model; let i = index" [index]="i"></foo>
Then use the input in the view
selector: 'foo',
template: `<div>{{_index}}</div>`,
This would allow you to manage the index at the app.component level via the *ngFor, and pass it into the new element on the DOM as it is rendered... essentially avoiding the need to assign the index to the component variable, and also ensuring the true index is provided when the change cycle needs it, at the time of render / class initialization.
Stackblitz
https://stackblitz.com/edit/angular-ozfpsr?embed=1&file=src/app/app.component.html
One way is update the index value using a Macro-Task. This is essentially a setTimeout, but bear with me.
This makes your subscription from your StackBlitz look like this:
ngAfterContentInit() {
this.foos.changes.subscribe(() => {
// Macro-Task
setTimeout(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
}, 0);
});
}
Here is a working StackBlitz.
So the javascript event loop is coming into play. The reason for the "ExpressionChangedAfter..." error is highlighting the fact that changes are being made to other components which essentially mean that another cycle of change detection should run otherwise you can get inconsistent results in the UI. That's something to avoid.
What this boils down to is that if we want to update something, but we know it shouldn't cause other side-effects, we can schedule something in the Macro-Task queue. When the change detection process is finished, only then will the next task in the queue be executed.
Resources
The whole event loop is there in javascript because there is only a single-thread to play with, so it's useful to be aware of what's going on.
This article from Always Be Coding explains the Javascript Event Loop much better, and goes into the details of the micro/macro queues.
For a bit more depth and running code samples, I found the post from Jake Archibald very good: Tasks, microtasks, queues and schedules
The problem here is that you are changing something after the view generation process is further modifying the data it is trying to display in the first place. The ideal place to change would be in the life-cycle hook before the view is displayed, but another issue arises here i.e., this.foos is undefined when these hooks are called as QueryList is only populated before ngAfterContentInit.
Unfortunately, there aren't many options left at this point. #matt-tester detailed explanation of micro/macro task is a very helpful resource to understand why the hacky setTimeout works.
But the solution to an Observable is using more observables/operators (pun intended), so piping a delay operator is a cleaner version in my opinion, as setTimeout is encapsulated within it.
ngAfterContentInit() {
this.foos.changes.pipe(delay(0)).subscribe(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
});
}
here is the working version
use below code, to make that changes in the next cycle
this.foos.changes.subscribe(() => {
setTimeout(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
});
});
I really don't know the kind of application, but to avoid playing with ordered indexes , it is often a good idea to use uid's as index.
Like this, there is no need to renumber indexes when you add or remove components since they are unique.
You maintain only a list of uids in the parent.
another solution that may solve your problem , by dynamically creating your components and thus maintain a list of these childs components in the parent .
regarding the example you provided on stackblitz (https://stackblitz.com/edit/angular-bxrn1e) , it can be easily solved without monitoring changes :
replace with the following code :
app.component.html
<hello [(model)]="model">
<foo *ngFor="let foo of model;let i=index" [index]="i"></foo>
</hello>
hello.component.ts
remove changes monitoring
added foocomponent index parameter
import { ContentChildren, ChangeDetectorRef, Component, Input, Host, Inject, forwardRef, QueryList } from '#angular/core';
#Component({
selector: 'foo',
template: `<div>{{index}}</div>`,
})
export class FooComponent {
#Input() index: number = 0;
constructor(#Host() #Inject(forwardRef(()=>HelloComponent)) private hello) {}
getIndex() {
if (this.hello.foos) {
return this.hello.foos.toArray().indexOf(this);
}
return -1;
}
}
#Component({
selector: 'hello',
template: `<ng-content></ng-content>
<button (click)="addModel()">add model</button>`,
})
export class HelloComponent {
#Input() model = [];
#ContentChildren(FooComponent) foos: QueryList<FooComponent>;
constructor(private cdr: ChangeDetectorRef) {}
addModel() {
this.model.push({});
}
}
I forked this working implementation : https://stackblitz.com/edit/angular-uwad8c

Passing value to child component after it's added to the list on parent component - Angular

I'm working on small Angular project, and I want to pass value from parent to child after it is addedd to the list, so it goes like this:
new-component emits item to the list-component
and after it's addedd to the list, lets move it forward to view-component, so I can see recently addedd item in case I want to edit something--
And everything works fine until I want to see it on view-component (it's successfully added to the list etc.) here is my code:
on a new component item is emitted to list:
onSubmit() {
this._viewService.save(this.view)
.subscribe(
(data: View) => {
this.onSave.emit(data);
}
}
As list is parent list is listening for this emitter.
<view-new #addNew (onSave)="onAdd($event)"></view-new>
After it is emitted, its available in onAdd method, and until this part everything is ok:
onAdd(view: View) {
this.receivedView = view;
// And here I'm opening viewDisplay compnent which displaying info about recently addedd data
this.viewDisplay.show();
}
Also on my list which is parent component is also included display component, which needs to display info about recently addedd item:
<view-display #displayView [view]="receivedView"></view-display>
And when I open this component by calling this.viewDisplay.show();
#Input() view: View;
Which is part of display component is allwayas empty :
show() {
$('#' + this.id).modal('show');
console.log("added - selected:view", this.view);
}
this.view which should be filled onAdd method from parent and passed here is allways empty.. I dont know how..
Thanks guys
Cheers
you are updating field recievedView in a parent, which is bound to view field in a child. This binding is done by angular change detection mechanism. In your onAdd method you are trying to call viewDisplay.show() method synchroniously, before the change detection is done, and, because of this field is not updated yet. The simplest solution would be to assign the field of a child in place like
onAdd(view: View) {
this.receivedView = this.viewDisplay = view;
this.viewDisplay.show();
}
but there are surely better options which depend on your project
Angular needs to know that the value of view has changed. You usually do this by defining a setter. By defining a setter, changes to an input variable can be tracked. You also need to define a private property to save the value in.
private _view: any;
#Input()
get view(): any) {
return this._view;
}
set view(view: any) {
this._view= view;
// Do other stuff when "view" was updated
}
You can also handle the communiation via the service pattern.

Angular 2: How to detect changes in an array? (#input property)

I have a parent component that retrieves an array of objects using an ajax request.
This component has two children components: One of them shows the objects in a tree structure and the other one renders its content in a table format. The parent passes the array to their children through an #input property and they display the content properly. Everything as expected.
The problem occurs when you change some field within the objects: the child components are not notified of those changes. Changes are only triggered if you manually reassign the array to its variable.
I'm used to working with Knockout JS and I need to get an effect similar to that of observableArrays.
I've read something about DoCheck but I'm not sure how it works.
OnChanges Lifecycle Hook will trigger only when input property's instance changes.
If you want to check whether an element inside the input array has been added, moved or removed, you can use IterableDiffers inside the DoCheck Lifecycle Hook as follows:
constructor(private iterableDiffers: IterableDiffers) {
this.iterableDiffer = iterableDiffers.find([]).create(null);
}
ngDoCheck() {
let changes = this.iterableDiffer.diff(this.inputArray);
if (changes) {
console.log('Changes detected!');
}
}
If you need to detect changes in objects inside an array, you will need to iterate through all elements, and apply KeyValueDiffers for each element. (You can do this in parallel with previous check).
Visit this post for more information: Detect changes in objects inside array in Angular2
You can always create a new reference to the array by merging it with an empty array:
this.yourArray = [{...}, {...}, {...}];
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [].concat(this.yourArray);
The code above will change the array reference and it will trigger the OnChanges mechanism in children components.
Read following article, don't miss mutable vs immutable objects.
Key issue is that you mutate array elements, while array reference stays the same. And Angular2 change detection checks only array reference to detect changes. After you understand concept of immutable objects you would understand why you have an issue and how to solve it.
I use redux store in one of my projects to avoid this kind of issues.
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
You can use IterableDiffers
It's used by *ngFor
constructor(private _differs: IterableDiffers) {}
ngOnChanges(changes: SimpleChanges): void {
if (!this._differ && value) {
this._differ = this._differs.find(value).create(this.ngForTrackBy);
}
}
ngDoCheck(): void {
if (this._differ) {
const changes = this._differ.diff(this.ngForOf);
if (changes) this._applyChanges(changes);
}
}
It's work for me:
#Component({
selector: 'my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
export class MyComponent implements DoCheck {
#Input() changeArray: MyClassArray[]= [];
private differ: IterableDiffers;
constructor(private differs: IterableDiffers) {
this.differ = differs;
}
ngDoCheck() {
const changes = this.differ.find(this.insertedTasks);
if (changes) {
this.myMethodAfterChange();
}
}
This already appears answered. However for future problem seekers, I wanted to add something missed when I was researching and debugging a change detection problem I had. Now, my issue was a little isolated, and admittedly a stupid mistake on my end, but nonetheless relevant.
When you are updating the values in the Array or Object in reference, ensure that you are in the correct scope. I set myself into a trap by using setInterval(myService.function, 1000), where myService.function() would update the values of a public array, I used outside the service. This never actually updated the array, as the binding was off, and the correct usage should have been setInterval(myService.function.bind(this), 1000). I wasted my time trying change detection hacks, when it was a silly/simple blunder. Eliminate scope as a culprit before trying change detection solutions; it might save you some time.
Instead of triggering change detection via concat method, it might be more elegant to use ES6 destructuring operator:
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [...this.yourArray];
You can use an impure pipe if you are directly using the array in your components template. (This example is for simple arrays that don't need deep checking)
#Pipe({
name: 'arrayChangeDetector',
pure: false
})
export class ArrayChangeDetectorPipe implements PipeTransform {
private differ: IterableDiffer<any>;
constructor(iDiff: IterableDiffers) {
this.differ = iDiff.find([]).create();
}
transform(value: any[]): any[] {
if (this.differ.diff(value)) {
return [...value];
}
return value;
}
}
<cmp [items]="arrayInput | arrayChangeDetector"></cmp>
For those time travelers among us still hitting array problems here is a reproduction of the issue along with several possible solutions.
https://stackblitz.com/edit/array-value-changes-not-detected-ang-8
Solutions include:
NgDoCheck
Using a Pipe
Using Immutable JS NPM github

Error when using property that relies on ViewChildren

I have created a custom component that contains a form <address></address>. And I have a parent component that has an array of these:
#ViewChildren(AddressComponent) addressComponents: QueryList<AddressComponent>;
So the parent can contain a collection of these elements and the user can add and remove them based on the number of addresses they will be entering.
The parent also has a button to proceed after the user has entered all desired addresses. However, the <address> component must be filled out correctly so I have a public getter on the <address> component:
get valid(): boolen {
return this._form.valid;
}
Back to the button on the parent. It needs to be disabled if any of the <address> components are invalid. So I wrote the following:
get allValid() {
return this.addressComponents && this.addressComponents.toArray().every(component => component.valid);
}
And in the parent template:
<button [disabled]="!allValid" (click)="nextPage()">Proceed</button>
But angular doesn't like this because addressComponents are not defined in the parent until ngAfterViewInit lifecycle event. And since it immediately runs ngOnViewInit() I get two different values for the expression check which causes the error. (At least that's what I think is going on).
How do I use a property in my template that depends on ngAfterViewInit? Or what is the best way to inform my parent that all of its children are valid?
The Error Message:
Expression has changed after it was checked. Previous value: 'false'.
Current value: 'true'
Update:
So I console.loged the return value of allValid and noticed the first time it was undefined. This was to be expected as this.addressComponents are undefined until ngAfterInit. The next log it was true and this was surprising as I didn't have any <address> components on the page (yet). I am using mock data (all valid, though) in ngOnInit of the parent component to create a component. I did learn that ([].every... returns true on an empty array). So the third call to the console.log was returning false. Again, I am a little surprised because all my data is valid. On the 4th log it was returning true which is what I expected. So I'm assuming this final value being returned is what Angular disliked.
Anyway, I was able to sort of solve this. I don't know if I'm actually fixing the problem or just suppressing the error. I do not like this solution so I am going to keep the question open for a better solution.
get allValid() {
return this.addressComponents && this.addressComponents.length > 0 && this.addressComponents().toArray().every(component => component.valid);
}
So what I think is happening:
The first wave of change detection gets you a false for your function, then your parent component finds out this information after the view is instantiated (then returns true). In "dev" mode, Angular runs change detection twice to ensure that changes don't happen AFTER change detection (as change detection should detect all of the changes, of course!)
According to the answer found here:
Angular2 - Expression has changed after it was checked - Binding to div width with resize events
using AfterViewInit can cause these issues, as it may run after the change detection has completed.
Wrapping your assignment in a timeout will fix this, as it will wait a tick before setting the value.
ngAfterViewInit(){
setTimeout(_ => this.allValid = this.addressComponents && this.addressComponents.toArray().every(component => component.valid));
}
Due to these reasons, I would not use a getter on a template variable like that, as the view initializing may change the value after change detection has finished.
If I understand, you'll probably need to come up with a way in the parent to track how many instances of the child there are, and the child will need an EventEmitter that informs the parent when it's valid or becomes invalid.
So in the parent you could use an array to track how many address instances there are..
Parent Component
addressForms: Array<any> = [{ valid: false }];
addAddressForm() {
this.addressForms.push({ valid: false ));
}
checkValid() {
// somehow loop through the addressForms, make sure all valid
let allValid: boolean = false;
for (var i = this.addressForms.length - 1; i >= 0; i--) {
if (this.addressForms[i].value && allValid === false)
allValid = true;
}
return allValid;
}
Parent Template
<div *ngFor="let form of addressForms; let i = index">
<address (valid)="form.valid = true" (invalid)="form.valid = false"></address>
</div>
<button [disabled]="checkValid()">Next</button>
Address Component
#Output() valid: EventEmitter<any> = new EventEmitter();
#Output() invalid: EventEmitter<any> = new EventEmitter();
isValid: boolean = false;
check() {
// call this check on field blurs and stuff
if ("it's valid now" && !this.isValid) {
this.isValid = true;
this.valid.emit(null);
}
if ("it's not valid anymore" && this.isValid) {
this.isValid = false;
this.invalid.emit(null);
}
}
That's the basic idea anyway, with some holes that are obvious enough to fill in. Hope that has some relevancy with what you're doing and I understood the question to begin with. Good luck!
I faced the same issue when have been using that realy handy pattern :( Only short way I found atm to solve it is the next kind of a hack:
#ViewChild(DetailsFormComponent) detailsForm: DetailsFormComponent;
isInitialized = false;
get isContinueBtnEnabled(): boolean {
return this.isInitialized && this.detailsForm?.isValid();
}
and
ngAfterViewInit() {
setTimeout(() => { // required
this.isInitialized = true;
});
}

Categories

Resources