Angular 2 change detection on array push of objects - javascript

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

Related

angular10: How to detect complex input object in array change

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

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,
}))
: [];

FormArray and other arrays of child Component (#ViewChildren) are empty

I have #ViewChildren child Component(BookFormComponent) inside a parent component(LibraryComponent). In my parent component I make a service call to get one BookData object.
I give the DookData object to a method of the child component initBookData(...). I want the child component to use the BookData to initialize its form controls. The BookData has an attribute selectedTypes which contains an array of books the user has already selected. I use the array to check its checkboxes.
There are 10 checkboxes and for instance if a user has 5 elements in the selectedTypes array then those 5 elements has to be checked out of the 10 checkboxes when the view is displayed.
The issue am having now is the form controls for name and color are initialized with the values from the BookData object but the checkboxes are not checked(selected) when the view is displayed. I did console.log()'s inside initSelectedTypes(....) of the child component and the lengths of the arrays are 0's meanwhile the child component uses the same arrays to display the checkboxes in the UI but when it has to use the same array to check(select) some of the checkboxes then the lengths are 0's.
My understanding is that the <book-form #book></book-form> in the parent component UI is the same as the attribute #ViewChildren(BookFormComponent) book: QueryList<BookFormComponent>; in the component class. So since the view is displayed then when I call a method on the attribute (book) then I expect all attributes of (book) to be initialized as well. I don't expect the arrays to be empty. All checkboxes are displayed correctly in the view but when I call initBookData(...) the arrays are empty.
I am using #ViewChildren because I tried #ViewChild and I was getting "undefined" (so could not even call the child's methods)
(I have omitted certain things in the code snippet to conserve space):
interface BookData {
name?: string,
color?: string,
selectedTypes?: Array<string> // this array contains the types a user has selected already
}
// PARENT COMPONENT CLASS
class LibraryComponent implements AfterViewInit, {
#ViewChildren(BookFormComponent) book: QueryList<BookFormComponent>;
// ADDITIONAL CHILDREN FOR OTHER mat-step omitted for clarity
bookData: BookData = {}
ngAfterViewInit(): void {
this.getBookData();
this.book.changes.subscribe((algemen: QueryList<BookFormComponent>) => {
book.first.initBookData(this.bookData);
});
}
// this method returns one book from the server and assigns it to "this.bookData"
getBookData() {
bookdataService.getBookData().subscribe(book => {
this.bookData = book;
});
}
}
// PARENT COMPONENT UI
<mat-horizontal-stepper #stepper linear>
<mat-step [stepControl]="book.bookForm">
<ng-template matStepLabel>Book</ng-template>
<book-form #book></book-form>
</mat-step>
<mat-step>
// ADDITIONAL STEPS ARE OMITTED FOR CLARITY
</mat-step>
</mat-horizontbal-stepper>
// CHILD COMPONENT CLASS
Component({
selector: 'book-form'
})
class BookFormComponent {
bookForm: FormGroup;
name = new FormControl('');
color = new FormControl('');
// Checkboxes for types of books a user can select. user can select multiple checkboxes
types = new FormArray([]);
optionsTypes = [];
ngOnInit(): void {
this.bookForm = this.fb.group({
name: this.name,
color: this.color
});
this.initializeTypesCheckboxes();
}
// This function will create 10 checkboxes that a user can select multiple of them
private initializeTypesCheckboxes() {
this.bookservice.getTypeOptions().subscribe(results => {
// the results from the server is array of strings of 10 elements
// eg: ["Maths", "English", "Chemistry", ...]
this.optionsTypes = results;
// we create checkboxes based on the number of types we get from the server
const typeCheckboxes = this.optionsTypes.map(t => new FormControl(false));
// we push the the checkboxes to the "this.types" form array
typeCheckboxes.forEach(type => this.types.push(type));
});
}
// This method is called from the parent component
public initBookData(bookData: BookData) {
this.naam.setValue(bookData.naam);
this.color.setValue(bookData.color);
this.initSelectedTypes(this.types, this.optionsTypes, bookData.selectedTypes);
}
// this method will use the already "alreadySelectedTypes" array to pre-select some of the checkboxes.
private initSelectedTypes(formArray: FormArray, optionsTypes: Array<string>, alreadySelectedTypes: Array<string>) {
for (let i = 0; i < formArray.controls.length; i++) {
for (const type of alreadySelectedTypes) {
if (optionsTypes === type) {
formArray.controls[i].patchValue(true);
}
}
}
console.log("LENGTH-formArray:", formArray.length); // i get O
console.log("LENGTH-optionsTypes:", optionsTypes.length); // i get O
}
}
What am I doing wrong?
Have you tried ContentChildren?
#ContentChildren(BookFormComponent) book: QueryList<BookFormComponent>;
It's unclear to me why you're using#ViewChildren at all. Unless I'm missing something about what you're trying to do, I think you're making your life more complicated than it needs to be.
The Parent Class
Your parent component class can be stripped down to just:
// PARENT COMPONENT CLASS
class LibraryComponent implements OnInit {
// ADDITIONAL CHILDREN FOR OTHER mat-step omitted for clarity
book: BookData = {};
form: FormGroup = new FormGroup()
ngOnInit(): void {
this.bookService.getBookData.subscribe(book => (this.book = book));
}
//This is to get the form group from a child Output event and use it in stepper.
onFormReady(form: FormGroup): void {
this.form = form;
}
}
Parent template
The way to pass data to child components from their parent is through an #Input directive. So, your parent template would look something like:
<mat-horizontal-stepper #stepper linear>
<mat-step [stepControl]="form">
<ng-template matStepLabel>Book</ng-template>
<book-form (formGroup)="onFormReady($event)" [bookData]="bookData"></book-form>
</mat-step>
<mat-step>
// ADDITIONAL STEPS ARE OMITTED FOR CLARITY
</mat-step>
</mat-horizontbal-stepper>
Child Class
Your child class can take care of setting up the form from its input, and then send the form group back up to the parent as a #Output. It would look something like this:
// CHILD COMPONENT CLASS
Component({
selector: 'book-form'
})
class BookFormComponent {
#Input('bookData') bookData: BookData
#Output('formGroup') formEmitter = new EventEmitter<FormGroup>();
bookForm: FormGroup;
options: string[]
ngOnInit() {
// get and store type options at start
this.booksService.getTypeOptions(options => {
// once options are ready.
// If options is empty, then the function on your service isn't working.
this.options = options;
this.bookForm = this.initializeForm(); // make the form
this.formEmitter.emit(this.bookForm); //send it to parent
})
}
initializeForm(): FormGroup {
const { name, color, selectedTypes } = this.bookData;
const form = this.fb.group({
name: new FormControl(name),
color: new FormControl(color),
types: new FormArray([])
})
// One form group for each possible option. Each group has a single control named after the option it represents.
this.options.forEach(option => {
let value = selectedTypes.includes(option);
form.types.push(this.fb.group({[option]: new FormControl(value)}));
})
return form;
}
}
I wouldn't expect this to work as is, but it's more or less the direction you should go in. It strips out a lot of the fat, makes your code easier to understand, and sends data between components the way you're supposed to.
The docs have a very good section that goes over the methods for doing this:
https://angular.io/guide/component-interaction

Cannot set value to object property in Typescipt. Cannot set property 'ismain' of undefined

bellow the class i initialize an object of type Photo, which photo is an interface with some attributes. And then i'm trying to use array filter to filter photos.
The filter return a copy of the photos array but i specify which photo do i want so i will have an array with 1 index inside. And then, i try to set a property inside the object to false, and the "Cannot set property 'ismain' of undefined in angular" occurs. Any ideas why?
Here is my code.
setMainPhoto(photo: Photo) {
this.userService
.setMainPhoto(this.authService.decodedToken.nameid, photo.id)
.subscribe(
() => {
// We use the array filter method to filter the photos apart from the main photo
// Filter returns a copy of the photos array. Filters out anything that it doesn`t match in the p
this.currentMain = this.photos.filter(p => p.ismain === true)[0];
this.currentMain.ismain = false;
photo.ismain = true;
this.alertify.success('Successfully set to profile picture');
},
error => {
this.alertify.error('Photo could not be set as profile picture');
}
);
}
In the above method the error occurs.
And below is my Photo interface.
export interface Photo {
id: number;
url: string;
dateadded: Date;
ismain: boolean;
description: string;
}
ERROR
UPDATE
PROBLEM SOLVED
The response from the server needs to match with the object attributes. So i had to change the property interface to match the JSON object coming back from the server.
Try this
setMainPhoto(photo: Photo) {
this.userService
.setMainPhoto(this.authService.decodedToken.nameid, photo.id)
.subscribe(
() => {
// We use the array filter method to filter the photos apart from the main photo
// Filter returns a copy of the photos array. Filters out anything that it doesn`t match in the p
this.currentMain = this.photos.filter(p => p.ismain === true)[0];
if (this.currentMain) {
this.currentMain.ismain = false;
photo.ismain = true;
this.alertify.success('Successfully set to profile picture'); }
},
error => {
this.alertify.error('Photo could not be set as profile picture');
}
);
}
If this works, then the filter does not return anything.
Interesting, I've had this come up before too. Typescript is quite strict with capitalization and if you don't match the json object exactly, it won't work, create a new property, etc.
I've found it helpful to take a look at the json return before building my typescript model, to make sure I'm matching the object being sent.

Polymer 2.0 Data-Binding Array

I'm trying to show the list of all the candidates under their corresponding parties I have stored in my Firebase database. I am using Polymer 2.0 Starter-Kit.
Here's the code:
_checkParties() {
var candidate_list = []
firebase.database().ref("/candidates/"+this.party.$key).once("value", function(snapshot) {
snapshot.forEach(function(snap) {
var data = {"key": snap.key, "data": snap.val()}
var results = Object.keys(data["data"]).map(function(key) {
return {
name: key,
value: data["data"][key],
key: data["key"]
};
})
candidate_list.push(results[0])
})
})
this.candidates = candidate_list;
}
Basically I'm querying from my firebase the children of that path, then appended them to my var candidate_list. Outside the query I then set my this.candidates to the array var candidate_list.
Here's the code to my template:
<template is="dom-repeat" items="{{ candidates }}" as="candidate">
<div class="card">
<p>[[candidates]]</p>
<h3>[[candidate.name]] - [[candidate.value]]</h3>
<paper-button raised style="color: yellow; background-color: black;" on-tap="_castVote">Vote</paper-button>
</div>
</template>
The code to my static properties:
candidates: {
type: Array,
value: []
}
Then my constructor:
constructor() {
super();
}
I tried adding this to my constructor but it doesn't seem to affect anything:
constructor() {
super();
this.candidates = [];
}
The result should be that the cards in my template dom-repeat show all the candidates. Instead I'm getting nothing is being shown.
But when I console.log I could see all my candidates.
I am not sure where your _checkParties is being called, but it sounds like Polymer isn't aware that you updated the candidates array.
I think that the first attempt could be to use the set method, to be sure it's notified of the update, as described here. So that would mean to replace your last statement in the method. Instead of
this.candidates = candidate_list;
you would have
this.set('candidates', candidate_list);
Another option, that seems to be very well suited for what you're doing would be to have the candidates list as a computed property. So you would just add to your property definition the 'computed' key:
candidates: {
type: Array,
value: [],
computed: '_checkParties(candidatesResponse)',
}
and in your method you would have to return the list, instead of overwritting the property, so
return candidate_list;
instead of the same line mentioned above.

Categories

Resources