angular10: How to detect complex input object in array change - javascript

I have an array that I use DoCheck and IterableDiffer to listen to changes in my code. When the array is changed, but when a property inside one of the objects in the array changes I don't get notified. I tried using KeyValueDiffer for each object in the array types but it doesn't work in ngDoCheck. Any ideas?
In the parent:
<comp [types]="types"></comp>
readonly: boolean = false;
types: {
readonly: boolean;
title: string;
value: any;
}[] = [{
title: 'title1',
readonly: this.readonly,
value: 1
}, {
title: 'title2',
readonly: this.readonly,
value: 2
}];
ngOnInit() {
setTimeout(() => {
this.readonly = true;
}, 5000)
}
In component comp:
constructor(
private differsService: KeyValueDiffers
) {};
#Input() types: any[] = [];
ngOnInit() {
this.differ = this.searchTypes.reduce((t, c, i) => {
t[`types_${i}`] = this.differsService.find(c).create();
return t;
}, {});
}
ngDoCheck(): void {
if (this.differ) {
Object.keys(this.differ).map((key: string) => {
const k = key.split('_');
const state = k.length === 1?this[k[0]]:this[k[0]][k[1]];
const changes = this.differ[key].diff(state);
if (changes) {
console.log(key, changes);
}
})
}
}

First point to be made is that this kind of change detection can be unnecessarily expensive and complicated. I'd recommend reading up on RxJS and how it works to handle changes in data.
In your specific example, however, the problem is not with your key differs, but with the logic of how you're handling readonly
In javascript, primitives (such as booleans) are not passed by reference, but by value.
This means that when you set the following:
readonly: this.readonly
the value in the type for readonly is set to false, and that's it. If later you change this.readonly the value within type is not changed. If you want to keep the connection between the two "live", you need to pass by reference, with an object, for example. something like
this.readonly = { status: false }
Then when you do
this.readonly.status = true
that value will be changed in the types as well.
Here's a stackblitz demonstrating it in action (and no need for keydiffers):
https://stackblitz.com/edit/angular-ivy-d8gige?file=src/app/app.component.ts

In Angular, change detection is triggered on assignment. This means that you need to assign new value to bound variables or objects to trigger the change detection. So, instead of changing properties on objects only. Try to reassign the array again afterwards to trigger detection:
types = [...types];
Try assigning that in the parent component when updating the values in types array
<comp [types]="types"></comp>
This article might help you in understanding that concept:
https://www.digitalocean.com/community/tutorials/angular-change-detection-strategy

Related

Vue on changing array object trigger get method to check for changes

I have a button component that looks like this
<template>
<button
class="o-chip border-radius"
:class="{
'background-color-blue': theValue.isSelected,
'background-color-light-grey': !theValue.isSelected,
}"
#click="onSelection(theValue)"
>
{{ theValue.displayText }}
</button>
</template>
And when it is pressed it sets it isSelected state and emit an event to the parent component
private onSelection() {
this.theValue.isSelected = !this.theValue.isSelected;
this.$emit('selected', this.theValue);
}
So far so good the issue is in the parent component. Here I go through all the items that is the array that creates the button components above. The .value property is the unique identifier here so what I do is I look through the array with the item sent from the button component, then when found i use splice to trigger reactivity to replace that array object with the one sent from the button component. When i console.log this it works it shows that isSelected property now is true.
private selectedItem(item: Item) {
var index = this.itemSelectOptions
.map((p) => p.value)
.indexOf(item.value);
this.itemSelectOptions.splice(index, 1, item);
console.log(this.itemSelectOptions);
}
But then i have this get method that checks for anyChanges on this array and other things and then render UI based on true/false here. The issue is that when this array get changed the anyChanges method does not react and remains false.
private get anyChanges(): boolean {
console.log(this.itemSelectOptions);
return this.itemSelectOptions!.some((p) => p.isSelected);
}
How do i make it so that the anyChanges get method reacts on the changes made to itemSelectOptions which is also an get function
private get itemSelectOptions(): Item[] {
return this.items
? this.items.map((item) => ({
value: item.id.toString(),
displayText: item.displayName.toString(),
isSelected: false,
}))
: [];
}
What you want is a watcher on itemSelectOptions
watch: {
question(newOptions, oldOptions) {
this.anyChanges(newOptions)
}
},
guess it will look smthing like this ^^
https://vuejs.org/guide/essentials/watchers.html#basic-example
The reason this was not working was that since itemSelectOptions where a get function it should/can not be modified. So changing it into a private variable and initializing it solved it like this.
private itemSelectOptions: Item[] = [];
and then initializing
this.itemSelectOptions = this.items
? this.items.map((item) => ({
value: item.id.toString(),
displayText: item.displayName.toString(),
isSelected: false,
}))
: [];

Specifying types with TypeScript, when <Array> has different kinds of structure

I am kind of new to TypeScript and I am trying to get rid of all any types.
Problem:
Within the React Component, I loop over an array of objects and extract a key/value pair.The components receives tags, tagKeys props as follows:
tags: [{ name: "Some tag" }] OR [{ platform: { name: "Some tag" } }];
tagKeys: ["name"] OR tagKeys: ["platform", "name"]
If I run this script (below), I get this error in the console:
Element implicitly has an 'any' type because index expression is not of type 'number'
type Props = {
tags: any[];
tagKeys: string[];
};
const Tags: React.FC<Props> = ({ tags, tagKeys }: PropsWithChildren<Props>) => {
const renderTags = (): React.ReactNode => {
return tags.map(tag => {
let key = "";
// Loop through tagKeys in the tags array to extract the tag
for (let i = 0; i < tagKeys.length; i++) {
key = i === 0 ? tag[tagKeys[i]] : key[tagKeys[i]];
}
return <Tag key={`tag-${key}`} tag={key} />;
});
};
return <StyledTags>{renderTags()}</StyledTags>;
};
If I change the line inside the for loop to...
key = i === 0 ? tag[tagKeys[i] as any] : key[tagKeys[i] as any];
...the script runs but I want to get rid of the any type.
Question:
Is there a way to set the type without specifying how the received props array tags will look?
I want to be able to pass arrays with different kinds of structure and extract a key/value pair.
There two issues to watch that we'll need to be clear about.
First, you asked:
Is there a way to set the type without specifying how the received props array tags will look?
type Props = {
tags: any[]; // This is the answer to your direct question
tagKeys: string[];
};
Secondly, having handled the above, TypeScript is actually complaining about your index.As you'll agree, reactjs uses the unique key to render each <Tag>.
// TypeScript is actually warning you about this.
// Note that initially, `key` is of type `string` (implicitly or indirectly )
let key = "";
// Within your loop, you assign `key` to be of type `object`
key = i === 0 ? tag[tagKeys[i]] : key[tagKeys[i]];
// So the above line says that `key` can be `string` or `object`
// Thus TypeScript tells you that...
// implicitly your index (key) expression is of type any
To handle that TypeScript error, update as follows:
let key: any = "";
Kindly make the necessary updates, and let me know how it goes for you.
key can be either a string or an object during it's lifetime, so my first intuition would be to declare it as such:
let key: object | string = "";
But TS is not happy with that, so my second try would be:
type NestedObject = { [key: string]: NestedObject } | string
let key: NestedObject = '';
key = { platform: { name: 'dd' } }
key = key.platform.name
But TS is not happy with the last line either, so at this time I would just give up:
let key: any = '';

Return a merged object composed by two different Observables with dependencies with RxJS

I'm going to explain the context I have before explain the problem design a service with Angular and RxJS.
I have one object with this model:
{
id: string;
title: string;
items: number[];
}
I obtain each object of this type (called "element") through GET /element/:id
Each element has an items: number[] that contains an array of numbers and each number has an URL so I do a new GET /element/:id/:itemNumber for each number and return the items detail. That item detail model is like:
{
idItem: string;
title: string;
}
So in my service I want to serve to the components a method that it will return an Observable, it will obtain an array of objects, where each object has the element model with one addition property that will be an array of its detailed items. So, I'm going to show what I have:
The service:
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
map(element => this.getAllItemsDetail(element))
}
getAllItemsDetail(element: Element): any {
return of(...element.items).pipe(
map(val => this.getItemDetail(element.id, val)),
concatAll()
);
}
My problem is, I'm not understanding how I can, in RxJS, after the map(element => this.getAllItemsDetail(element)) in the getElement method, merge the items array returned by it and the previous element I have before the map that has the element object. I need to add "anything" after that map to compute an Object.Assign to merge the both objects.
EDIT
With the answer of #chiril.sarajiu, he gives me more than one clue, with switchMap I can map the observable (map is used for objects, "do a compute with the result object"). So my code, it's not the most pretty version I know..., I will try on it, but it works. So the final code looks like this:
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
switchMap(element => this.getAllItemsDetail(element)),
map(result => { return Object.assign({}, result.element, {itemsDetail: result.items})})
);
}
getAllItemsDetail(element: Element): any {
const results$ = [];
for (let i = 0; i < element.items.length; i++) {
results$.push(this.getItemDetail(element.id, element.items[i]));
}
return zip(...results$).pipe(
map(items => { return Object.assign({}, { element, items })})
);
}
Be aware of the possibility about zip will not emit values if the results$ is empty, in that case you can add of(undefined) to the results$ array and after take into account that items will be [undefined]
To map another observable you should use switchMap function instead of map
So
getElement(id: string): any {
return this.http.get<Element>(this.basePath + `/element/${id}`).pipe(
switchMap(element => this.getAllItemsDetail(element))
}
UPDATE
Consider this medium article

React setState for nested object while preserving class type

Extending from this question React Set State For Nested Object
The way to update nested state is to decompose the object and re-construct it like the following:
this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
However, this is a problem for me, as it will not preserve the component class (More detail below). I am aiming for my state to be structured like the following:
this.state = {
addressComponent: {
street: '',
number: '',
country: ''
},
weather: {
/* some other state*/
}
}
To make my life easier, I created a simple address class that builds the object for me and have some other utility function such as validation.
class AddressComponent {
constructor(street, number, country) {
this.street = street;
this.number = number;
this.country = country;
}
validate() {
if (this.street && this.number, this.country) {
return true;
}
return false;
}
}
This allows me to do is change the initialization of state to be:
this.state = {
addressComponent : new AddressComponent(),
weather: new WeatherComponent();
}
this will allow my view component to perform nice things like
if (this.state.addressComponent.validate()) {
// send this state to parent container
}
The problem with this approach is that if I want to mutate one single piece of information like country and use the above Stack Overflow approach such as:
this.setState({addressComponent: {...addressComponent, country: 'bluh'}})
Doing this will mean that the resolting addressComponent is no longer part of AddressComponent class hence no validate() function
To get around it I cound recreate a new AddressComponent class every time like:
this.setState({addressComponent: new AddressComponent(this.state.street, this.state.number, 'bluh');
But this seems weird.
Am I doing this wrong? Is there a better approach? Is it acceptable to use classes like this with react?
It's undesirable to use anything but plain objects for React state to avoid situations like this one. Using class instances will also make serialization and deserialization of the state much more complicated.
The existence of AddressComponent isn't justified, it doesn't benefit from being a class.
The same code could be rewritten as functional with plain objects:
const validateAddress = address => !!(street && address.number && address.country);
...
if (validateAddress(this.state.address)) {
// send this state to parent container
}
I think what you said about reinstantiating your class each time you update it in your state is the cleanest way to ensure your validate() method is called at that time. If it were me, I would probably write it as:
const { addressComponent } = this.state;
this.setState({ addressComponent: new AddressComponent(addressComponent.street, addressComponent.number, 'bluh') });
You can create a reusable function to update a complex state like this.
updateState = (option, value) => {
this.setState(prevState => ({
addressComponent: {
...prevState.addressComponent,
[option]: value
}
})
}

Angular 2 change detection on array push of objects

I have a 'Schedule' typescript class and a 'selectedSchedule' variable of this 'Schedule' type.
Schedule:
export class Schedule{
scheduleId: string;
sheduleDate: Date = new Date();
scheduleName: string;
jobs: Job[];
unscheduledEmployeesCount: number = 0;
idleEquipmentCount: number = 0;
unscheduledTrucksCount: number = 0;
}
I'm binding this variable and it's properties in the HTML by utilizing interpolation. My problem is I'm using a *ngFor to iterate and display each 'job'...
<div class="job" *ngFor="let j of selectedSchedule.jobs">
<div [ngClass]="{'hasEmployees': j.jobEmployees.length > 0 }">
<div *ngFor="let e of j.jobEmployees">
{{e.employee['name'] !== null ? e.employee['name'] : e.name}}
</div>
</div>
</div>
whenever the user triggers the 'addJob()' method by clicking a button, the newly created 'job' doesn't get detected by Angular and results in the properties of the 'job' to be null, most notably the 'jobEmployees' of each 'job'. I understand Angular doesn't detect changes to an array when you push/remove objects out of the box. I'm looking for a solution to pick up on these changes when a user adds a new job. I have tried to reassign the 'selectedSchedule' and it's 'jobs' property with no success. Also have tried to slice() the jobs array with the thought of 'recreating' the array.
I'm currently making an extra http request to get the schedule's jobs again, which seems to prevent the null property problems, but I'm hoping to find a more efficient solution.
The addJob() method in component:
addJob() {
var newJob: Job;
this.data.addJob(this.selectedSchedule.scheduleId).subscribe(addedJob => {
this.selectedSchedule.jobs.push(addedJob);
this.data.loadSchedules().subscribe(success => {
if (success) {
this.selectedSchedule = this.data.schedules.find(schedule => schedule.scheduleId === this.selectedSchedule.scheduleId);
}
});
});
}
addJob() method in data service:
addJob(scheduleId: string) {
return this.http.get("/api/job/addjob", { headers: this.headers, params: { scheduleId: scheduleId } })
.map((job: Job) => {
return job;
});
}
'Job' class:
export class Job {
jobId: string;
jobName: string;
jobNumber: string;
jobEmployees: Array<Employee> = new Array<Employee>();
jobEquipment: Array<Equipment> = new Array<Equipment>();
jobTrucks: Array<Truck> = new Array<Truck>();
}
You are looking for ChangeDetectorRef,Normally this is done by Angulars change detection which gets notified by its zone that change detection should happen. Whenever you manually subscribe in a component, angular marks the component instance as dirty whenever the subscribed stream emits a value.
Try calling it manually as follows,
this.selectedSchedule = this.data.schedules.find(schedule => schedule.scheduleId === this.selectedSchedule.scheduleId);
this.cdRef.detectChanges();

Categories

Resources