Edit Form Dialog Not Prepopulating

Viewed 140

I am using Angular 11 and Ionic as my UI library. The issue that I'm having is that I created an upsert dialog component and while the edit form returns the correct value when I get the value manually from the console, the selected item is not reflected in the UI when I pass it an existing object.

This is my component template:

<ion-card>
    <ion-card-header>
        <ion-card-title>{{title}} Car Seat</ion-card-title>
    </ion-card-header>
    <ion-card-content>
        <form [formGroup]="form" (submit)="save()">
            <app-input type="select" formControlName="CarSeatTypeId" label="Car Seat Type" [selectItems]="carSeatTypes" dataValue="CarSeatTypeId" textValue="CarSeatTypeName"></app-input>
            <div Flex justifyContent="flex-end">
                <ion-button type="submit">Save</ion-button>
                <ion-button type="button" color="light" (click)="close()">Close</ion-button>
            </div>
        </form>
    </ion-card-content>
</ion-card>

This is the component typescript:

import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

import { ICarSeat } from '../car-seats.interface';
import { ICarSeatType } from '../../core/interfaces/car-seat-type.interface';
import { ModalController } from '@ionic/angular';

@Component({
    templateUrl: './upsert-car-seat.component.html'
})
export class UpsertCarSeatDialogComponent implements OnInit {

    @Input() carSeatTypes: ICarSeatType[] = [];
    @Input() existing: ICarSeat;
    form: FormGroup;

    get title(): string {
        return (!this.existing ? 'Create' : 'Edit');
    }

    constructor(private formBuilder: FormBuilder,
                private modalController: ModalController) { }

    ngOnInit(): void {
        this.form = this.buildForm();
    }

    close(): void {
        this.modalController.dismiss();
    }

    save(): void {
        if (this.form.invalid) {
            Object.keys(this.form.controls).forEach(key => {
                this.form.get(key).setValue(this.form.get(key).value);
            });
            return;
        }
        this.modalController.dismiss(this.form.value);
    }

    private buildForm(): FormGroup {
        return this.formBuilder.group({
            CarSeatTypeId: [this.existing?.CarSeatTypeId, Validators.required]
        });
    }
}

This is what I'm doing to open the dialog:

async edit(carSeat: ICarSeat): Promise<void> {
    const modal = await this.modalController.create({
        component: UpsertCarSeatDialogComponent,
        componentProps: {
            carSeatTypes: this.carSeatTypes,
            existing: carSeat
        }
    });
    modal.onWillDismiss().then(results => {
        if (results?.data) {
            carSeat.CarSeatTypeId = results.data.CarSeatTypeId;
            this.carSeatsService.updateCarSeat(carSeat).subscribe(result => {
                this.alertService.showConfirmation({
                    header: 'Car Seat Updated',
                    message: `The ${carSeat.CarSeatTypeName} has been updated.`,
                    buttons: [
                        { text: 'Close' }
                    ]
                });
            });
        }
    });
    modal.present();
}

And this is the app-input template (it ultimate gets converted to an ion-select):

<!-- dropdown -->
<ng-container *ngIf="elementType === 'ion-select'">
    <ion-item>
        <ion-label position="stacked">{{label}}</ion-label>
        <ion-select [(ngModel)]="value"
                    [cancelText]="cancelText"
                    [compareWith]="compareWith"
                    [disabled]="disabled"
                    [interface]="interface"
                    [interfaceOptions]="interfaceOptions"
                    [mode]="mode"
                    [multiple]="multiple"
                    [name]="name"
                    [okText]="okText"
                    [placeholder]="placeholder"
                    [selectedText]="selectedText"
                    [value]="value">
            <ion-select-option *ngFor="let item of selectItems" value="{{item[dataValue]}}">{{item[textValue]}}</ion-select-option>
        </ion-select>
        <ion-text *ngIf="ngControl?.control?.errors && ngControl?.control?.dirty"
                style="font-size:small;"
                color="danger"
                [innerHTML]="validationMessage"></ion-text>
    </ion-item>
</ng-container>

Here is a screenshot demonstrating the issue: enter image description here

I'm sort of at a loss as to what to do. I've tried moving the build form to the constructor and then to the OnAfterViewInit, I've tried to manually update the UI by calling changeDetectionRef.detectChanges(), and I've tried calling this.form.get('CarSeatTypeId').setValue(this.existing?.CarSeatTypeId) after this.buildForm() but none of that seems to matter.

From the screenshot, the form obviously has a value and it is referencing an existing CarSeatType. Any help is appreciated.

Update

I have also attempted to use a setTimeout with the timeout set to 3,000ms to manually set the control value. This does not work either.

2 Answers

Sounds like a change detection issue. Are you possibly mutating an object? Angular change detection does not like mutation and you should avoid it at all costs to ensure change detection works (i.e. to ensure that changes to values are correctly reflected in the UI).

When an object it mutated, the object reference does not change. Here's an example of mutation:

const obj = {a:2}
obj.a = 3 // MUTATION ALERT THIS IS BAD

To avoid this, there are many approaches. Heres a few easy ones:

let obj = {a:2}
obj = {a:3} // no mutation here, we are simply creating a new object

import {cloneDeep} from 'lodash-es'

let obj = {a:2}
obj.a = 3 // MUTATION ALERT THIS IS BAD
obj = cloneDeep(obj) // FIXES THE MUTATION PROBLEM BY CREATING A NEW OBJECT (AND THEREFORE A NEW OBJECT REFERENCE, which causes Angular change detection to kick in)

When dealing with object mutation I generally rely on lodash cloneDeep because its simple to use. But there are many other approaches to fix this.

Note mutation can also happen on arrays when using .push() .unshift() etc. Again, just cloning the array will fix the issue.

This might not be your problem but always worth ensuring this is not the issue when you face change detection problems.

Looking at your code there's a lot of superflous code and its difficult to identify where the problem lies but maybe start with just doing:

.setValue(cloneDeep(this.form.get(key).value))

wherever you use setValue

The underlying issue had to do with the wrapper we were using for the ion-select, a coworker made the following change which fixed the issue I was having:

<ng-container *ngIf="elementType === 'ion-select'">
    <ion-item>
        <ion-label position="stacked">
            <ion-text *ngIf="labelPrefix" [color]="labelPrefixColor">{{labelPrefix}}</ion-text>
            {{label}}
        </ion-label>
        <ion-select [(ngModel)]="value"
                    [cancelText]="cancelText"
                    [compareWith]="compareWith"
                    [disabled]="disabled"
                    [interface]="interface"
                    [interfaceOptions]="interfaceOptions"
                    [mode]="mode"
                    [multiple]="multiple"
                    [name]="name"
                    [okText]="okText"
                    [placeholder]="placeholder"
                    [value]="value">
            <ion-select-option *ngFor="let item of selectItems" value="{{item[dataValue]}}">{{item[textValue]}}</ion-select-option>
        </ion-select>
        <ion-text *ngIf="ngControl?.control?.errors && ngControl?.control?.dirty"
                style="font-size:small;"
                color="danger"
                [innerHTML]="validationMessage"></ion-text>
    </ion-item>
</ng-container>

and

compareWith: ((currentValue: any, compareValue: any) => boolean) | null | string | undefined = (currentValue: any, compareValue: any) => {
    return currentValue[this.dataValue] === compareValue[this.dataValue];
};

Specifically the diff between the old and new is that the compareWith binding was added and the selectedValue binding was removed.

Related