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

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

Related

NgRx Data service Update removes object prototype instead of updating the changed fields

When my NgRx DefaultDataService updates, it removes the type/prototype of the objects in the store. The objects are no longer of type Todo, but are simple objects with no prototype.
The objects come from the server as json objects conforming to the Todo interface. I pipe them to be entered into the store as Todo objects like this:
#Injectable()
export class TodosDataService extends DefaultDataService<ITodo> {
baseUrl;
constructor( http: HttpClient, httpUrlGenerator: HttpUrlGenerator) {
super("Todos", http, httpUrlGenerator);
}
getAll(): Observable<Todo[]> {
//HERE - the ITodo[] from server is mapped to Todo[], then saved to store
return super.getAll().pipe(
map(todosList => todosList.map(todoData => new Todo(todoData)))
);
}
But piping them on this service's update() override method doesn't work:
update(updatedTodo: Update<Todo>): Observable<Todo> {
return super.update(updatedTodo).pipe(map(data => new Todo(data)));
}
Note: I've also tried this using my own httpClient.put request manually without DefaultDataService's super call and several other ways.
The problem problem is that when ngrx applies the deltas (changes) to the object in the store, it somehow not a todo object anymore. Here's a screenshot of the console right after running the above statement:
Todo Prototype is gone
This is causing unexpected behavior.
TL;DR: How can we make sure the update does not remove the prototype? If that's not possible, is there a simple way to intercept updates before they hit the store, in order to provide custom implementation?
This behavior is intended see the following GitHub issue for more info.
https://github.com/ngrx/platform/issues/1641

Trigger a function once 2 inputs are set and then afterwards if either of the value changes in Angular

I have a component with 2 inputs (or mor) and I want to:
Trigger a method X the first time, when both value are set and exists
Trigger a method X each time, if either one of the both value changes
<some-cmp [itemid]="activeItemId$ | async" [userId]="activeUserId$ | async"></some-cmp>
Both values can change at any time, so I figured using rxjs to build a stream lets me control everything. My current solution seems a bit hacky and is difficult to test. I use 2 BehaviourSubjects and combineLatest with a debounceTime.
#Input() set itemId (id){this.itemId$.next(id)};
#Input() set userId (id){this.userId$.next(id)};
itemId$ = new BehaviourSubject$(null);
userId$ = new BehaviourSubbject$(null);
ngOnInt(){
combineLatest([
this.itemId$.pipe(filter(item=>item!===null)),
this.userId$.pipe(filter(item=>item!===null))
]).pipe(
debounceTime(10),
switchMap(...)
).subscribe(...)
}
So my question are
Is there a more elegant way to achieve this behavior?
Is there a way to avoid the debounceTime, which makes testing difficult?
The debounceTime is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.
You are right in using combineLatest, it will only emit the first time after each source has emitted once and then will emit any time any source emits.
Is there a way to avoid the debounceTime. [It] is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.
Maybe debounceTime isn't necessary due to the initial behavior of combineLatest; it will not emit the first time until all sources emit. However if you typically receive subsequent emissions from both sources that occur within a short timeframe, use of debounceTime may be an appropriate optimization.
Is there a more elegant way to achieve this behavior?
I think your code is fine. However, it may not be necessary to use BehaviorSubject since you aren't really using the default value. You could use plain Subject or ReplaySubject(1).
You could assign the result of your combineLatest to another variable and subscribe to that inside ngOnInit or use the async pipe in the template:
#Input() set itemId (id){ this.itemId$.next(id) };
#Input() set userId (id){ this.userId$.next(id) };
itemId$ = new Subject<string>();
userId$ = new Subject<string>();
data$ = combineLatest([
this.itemId$.pipe(filter(i => !!i)),
this.userId$.pipe(filter(i => !!i))
]).pipe(
debounceTime(10),
switchMap(...)
);
ngOnInit() {
this.data$.subscribe(...);
}
Angular provides ngOnChanges hook which can be used in this scenario. It'll trigger ngOnChanges method whenever there's a change in any of the inputs of the component.
Below is an example of how this can be achieved:
export class SomeComponent implements OnChanges {
#Input() itemId: any;
#Input() userId: any;
ngOnChanges(changes: SimpleChanges) {
const change = changes.itemId || changes.userId;
if (change && change.currentValue !== change.previousValue) {
this.doSomething();
}
}
private doSomething() {
// Your logic goes here
}
}
Your HTML will now look clean and you can get rid of async as well:
<some-cmp [itemid]="itemId" [userId]="userId"></some-cmp>

Why isn't my component object getting updated itself when anything in my globally shared service updates? Angular

I have this service:
export class RecipeService{
selectedRecipe: Recipe = 'xyz';
}
I have this component using this service:
export class RecipesComponent implements OnInit {
selectedRecipe: Recipe;
constructor(private recipeService: RecipeService) { }
ngOnInit(): void {
this.selectedRecipe = this.recipeService.selectedRecipe;
}
}
The service is defined in app.module.ts for injection, which means all components get the same instance.
My question is, whenever I update the selectedRecipe variable in one of my components, it doesn't get updated back in other components although it is referenced and hence I expect a change immediately.
What am I doing wrong?
It doesn't get updated because the new value is not "sent" to the already initiated angular components.
Instead you should use observables.
for example:
/* service */
private recipe = "xyz";
public recipeSubject: BehaviorSubject<string> = new BehaviorSubject(this.recipe);
// when changing the recipe
recipeSubject.next(this.recipe);
/* component */
this.service.recipeSubject.subscribe(res => this.recipe = res);
I googled and found out in one of the posts that its because of my object. The object (Recipe) in my service contains a primitive type, i.e string. If your object contains a primitive type, it isn't passed as a reference, hence a change in service object won't be reflected in component because they are now different.
Although I must clear that in case of array it worked perfectly fine even when my array contained objects which had primitive types. Changes were still reflected.

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

Why is this change emitted using Rxjs

I'm a little bit confused as why the following snipped works as expected.
The idea of this service is to have a list of strings where if you add a string, it is removed 5 seconds later. Rxjs is used here:
#Injectable()
export class ErrorService {
private errors: Array<string> = [];
private emitErrorsChanged = new Subject<any>();
public emitErrorsChanged$ = this.emitErrorsChanged.asObservable();
constructor() {
this.emitErrorsChanged$.delay(5000).subscribe(
() => {
if (this.errors.length > 0) {
this.errors.shift();
}
}
);
}
public emitErrorChange(error: string) {
this.errors.push(`${error}`);
this.emitErrorsChanged.next(this.errors);
}
}
An error component is subscribed to this service errorService.emitErrorsChanged$.subscribe(...) and shows the strings in a list. Other components/services add strings by this.errorService.emitErrorChange(error.message).
My question is: why are the removed errors (5s) emitted to the error component? The errors are just removed from the list this.errors.shift(); but the change is not emitted by this.emitErrorsChanged.next(this.errors);
The behavior occurs because you are passing reference to your object (list in this case). The changes made by this.errors.shift(); are not emitted, but I guess you can see current state of this.errors thanks to Angular's change detection. I have prepared a demo (click) so you can see that the object reference is passed in your case - what means that the list in subscription is the exactly same array list. To prevent it you can pass a copy of your list, e.g. using spread operator like in this example:
this.emitErrorsChanged.next([...this.errors]);

Categories

Resources