I have the following list of educations:
<cdk-virtual-scroll-viewport itemSize="5" >
<app-education-item *ngFor="let education of loadedEducations"
(isSelected)="changeSelected(education)"
[ngClass]="{ selected: education == loadedEducation }"
[education]="education"
(isRemoved)="removeEducation(education)"
></app-education-item>
</cdk-virtual-scroll-viewport>
and is the following component
<div [ngClass]="{ 'list-item-container-collapsed' : isCollapsed, 'list-item-container': !isCollapsed, 'unselected': !isActive, 'selected': isActive}" (click)="selectEducation()">
<div style="display: flex;">
<div >
<span >{{educationHeader}}</span>
<p >
<span>{{startDate}}</span> -
<span>{{endDate}}</span>
</p>
</div>
</div>
the has the following logic used to show the data taken from the parameter:
export class EducationItemComponent implements OnInit {
@Input()
education: Education;
isCollapsed = false;
isActive = false;
startDate: string;
endDate: string;
educationHeader: string;
educationDescription: string;
constructor() { }
ngOnInit(): void {
console.log(this.education);
this.startDate = this.education.startDate != '' ? formatDate(this.education.startDate, 'MMM yyyy', 'en-US')
: formatDate(new Date(), 'MM YYYY', 'en-US') ;
this.endDate = this.education.endDate != 'present' ? this.endDate = formatDate(this.education.endDate, 'MMM yyyy', 'en-US')
: this.education.endDate;
this.educationHeader = this.education.degree == undefined || this.education.description == undefined ? ''
: this.education.degree ' at ' this.education.school;
if (!this.education.description.enUS && this.education.description.nlNL) {
this.educationDescription = this.education.description.nlNL;
} else if (this.education.description.enUS) {
this.educationDescription = this.education.description.enUS;
}
}
I use a custom event to handle update
@Output() updatedValue: EventEmitter<any> = new EventEmitter<string>();
constructor() {}
ngOnInit(): void {}
fieldChanged(changes: SimpleChanges) {
this.updatedValue.emit(changes);
}
Then I have the following html that I use to manipulate the data:
<div >
<div >Update education</div>
<div>
<div >
<app-input-field
label="Institution"
[value]="loadedEducation.school"
(updatedValue)="loadedEducation.school = $event"
></app-input-field>
</div>
<div >
<app-input-field
label="Degree"
[value]="loadedEducation.degree"
(updatedValue)="loadedEducation.degree = $event"
></app-input-field>
</div>
</div>
</div>
however the updated data in the fields [value]="loadedEducation.school" (updatedValue)="loadedEducation.school = $event"
don't bind with the sub component so nothing is showing until I refresh and get the data from the DB.
What are the possibilities that I can try to implement?
I tried implementing ngOnChanges, that did not work.
CodePudding user response:
The root cause of the issue is that @Input()
is unable to detect changes to the internals of objects and arrays, as they are both reference types. Your education
property is an object, therefore changes that mutate properties directly such as education.school = 'newValue'
made in the parent component do not trigger any changes in the property @Input() education
of the child component
There are a few ways to deal with this, each with their pros and cons:
Pass only the properties you need as primitives
parent.component.ts
education: Education = <initial-value>
parent.component.html
<app-education-item
[school]="education.school"
[degree]="education.degree">
</app-education-item>
<app-input-field
label="Institution"
[value]="education.school"
(updatedValue)="loadedEducation.school = $event">
</app-input-field>
child.component.ts
export class EducationItemComponent implements OnChanges {
@Input() school: string;
@Input() degree: string;
ngOnChanges(changes: SimpleChanges): void {
// will emit whenever .school or .degree is changed in the parent
}
}
Pros:
- Simple and intuitive to use, works "as normal"
- No extra boilerplate required to send changes into the child component
Cons:
- Extra boilerplate required to receive changes into the child component. Becomes unwieldly as the number of
@Input
's grow - You lose the semantic coupling between the parent and child component which are really bound by a shared interface, i.e. the
Education
interface - Doesn't scale well if the properties are also reference types, in which case these would also need to be unpacked and passed as primitives
Rebuild object in the parent on change
parent.component.ts
education: Education = <initial-value>
updateEducation(educationProps: Partial<Education>): Education {
this.education = {
...this.education, // Note: You may want to 'deep clone' your object depending on how nested it is
...educationProps
}
}
parent.component.html
<app-education-item" [education]="education">
</app-education-item>
<app-input-field
label="Institution"
[value]="education.school"
(updatedValue)="updateEducation({ school: $event }">
</app-input-field>
child.component.ts
export class EducationItemComponent implements OnChanges {
@Input() education: Education;
ngOnChanges(changes: SimpleChanges): void {
// will emit whenever updateEducation() is called in the parent
}
}
Pros:
- Retains use of the
Education
interface, maintaining semantic coupling between the parent and child component - Promotes use of immutable objects which is a good practice for objects generally
- No extra boilerplate required to receive changes into the child component.
Cons:
- Extra boilerplate required to send changes into the child component, i.e. the creation of a superfluous
updateEducation()
function in the parent
Pass a reactive element into your child component, such as a BehaviorSubject
, and subscribe to the changes directly
parent.component.ts
educationSubject: BehaviorSubject<Education> = new BehaviorSubject<Education>( <initial-value> )
updateEducation(educationProps: Partial<Education>): Education {
const updatedEducation: Education = {
...this.education, // Note: You may want to 'deep clone' your object depending on how nested it is
...educationProps
}
this.educationSubject.next(updatedEducation}
}
parent.component.html
<app-education-item" [education]="education">
</app-education-item>
<ng-container *ngIf="educationSubject | async" as education>
<app-input-field
label="Institution"
[value]="education.school"
(updatedValue)="updateEducation({ school: $event }">
</app-input-field>
<ng-container>
child.component.ts
export class EducationItemComponent implements OnChanges {
@Input() educationSubject: BehaviorSubject<Education>;
}
child.component.html
<ng-container *ngIf="educationSubject | async" as education>
<p>{{ education.school }}</p>
</ng-container>
Pros:
- Gives full control over event emission/subscription. This can be beneficial for any other side effects you wish to trigger in response to
- Can easily be extended to work with many components, e.g. place
educationSubject
in a service and inject the same service into any components which require it - Also promotes use of immutable objects
- No extra boilerplate required to receive changes into the child component
Cons:
- Extra boilerplate required to send changes into the child component, i.e. the creation of a superfluous
updateEducation()
function in the parent - Typical limitations of working with reactive code such as only mutating via the stream, needing to be weary of unsubscribing (if not using
| async
) etc
CodePudding user response:
the loadedEducations list doesn't change when you change a property of an item from the list. try to refresh the list (this.loadedEducations = loadedEducations
) or use state management in your project