Home > Net >  Angular - How can I update a subcomponent that takes an input parameter and renders it correctly the
Angular - How can I update a subcomponent that takes an input parameter and renders it correctly the

Time:01-18

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
  }
}

Deep clone

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

  • Related