简体   繁体   中英

ExpressionChangedAfterItHasBeenCheckedError on Angular 6 while using <mat-tab>

I have the following Error with Angular 6

Component

<mat-tab-group [(selectedIndex)]="selectedTabIndex">
  <mat-tab label="Add">
    <ng-template matTabContent>
      <form [formGroup]="entityAddFormGroup">
        <dynamic-material-form [group]="entityAddFormGroup" [model]="entityAddFormCtlModelArray"></dynamic-material-form>
        <button (click)="buttonAddEntityClicked(entityAddFormGroup.value)">Add</button>
      </form>
    </ng-template>
  </mat-tab>
  <mat-tab *ngIf="entityEditFormGroup && currentEntity" label="Edit #{{currentEntity.id}}">
    <!-- TODO correct bug with -->
    <ng-template matTabContent>
      <form [formGroup]="entityEditFormGroup">
        <!-- <h2 i18n>Edit #{{currentEntity.id}}</h2> -->
        <dynamic-material-form [group]="entityEditFormGroup" [model]="entityEditFormCtlModelArray"></dynamic-material-form>
        <button (click)="buttonEditEntityClicked(entityEditFormGroup.value)">Save</button>
      </form>
    </ng-template>
  </mat-tab>
</mat-tab-group>

when I remove the second mat-tab the error disappears

Into other similar components, I didn't put the 2 forms into a mat-tab-group and mat-tab and don't have this error.

Spent a while discovering where is the difference.

Error into console

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-valid: true'. Current value: 'ng-valid: false'.

Environment

Angular CLI: 6.2.8
Node: 11.9.0
OS: linux x64
Angular: 

ts file (export class ElectricityRateListComponent extends SelectableEntitiesListComponent)

public displayedColumnsArray = [
    'select',
    'id',
    'energyRate',
    'mainTransmissionRate',
    'publicServiceRate',
    'validityStartDate',
    'validityEndDate',
    'electricityType',
    'city',
]; // Gives the order of the columns
public statusMessage: string = ''
public selectedTabIndex: number = 0


protected _elTypeAddSelect: DBEntitySelect<Enumerate> //ElectricityType: Enumerate
protected _elTypeEditSelect: DBEntitySelect<Enumerate> //ElectricityType: Enumerate

protected _cityAddSelect: DBEntitySelect<Enumerate> //City: Enumerate
protected _cityEditSelect: DBEntitySelect<Enumerate> //City: Enumerate

constructor(
    protected router: Router,
    public messageService: MessageService,
    protected logger: LoggerService,
    protected route: ActivatedRoute,
    protected entitiesService: ElectricityRateService,
    protected enumeratesService: EnumerateService,
    protected formBuilder: FormBuilder,
    public formService: DynamicFormService,
    iconRegistry: MatIconRegistry,
    sanitizer: DomSanitizer,
    // private location: Location
) {
    super(router, messageService, logger, route, entitiesService, formBuilder, formService, iconRegistry, sanitizer, new ElectricityRate());

    (...)
}



/**
* Common to add and edit forms
*
* @param aStrangeObject
*/
protected _getCommonFormControlModel(aStrangeObject: Enumerate): DynamicFormControlModel[] {
    let lEntity: ElectricityRate = new ElectricityRate().deserialize(
    aStrangeObject
    )
    console.debug(
    "-----getAddFormControlModel->",
    aStrangeObject,
    lEntity.validityStartDate.constructor.name,
    lEntity.validityEndDate.constructor.name
    )
    const result: DynamicFormControlModel[] = [
    new DynamicInputModel({
        id: "energyRate",
        label: "Energy Rate",
        value: lEntity.energyRate,
        inputType: DYNAMIC_FORM_CONTROL_INPUT_TYPE_NUMBER,
        min: ElectricityRate.MIN_ELECTRICITY_RATE,
        max: ElectricityRate.MAX_ELECTRICITY_RATE,
        placeholder: "Energy Rate"
    }),
    new DynamicInputModel({
        id: "mainTransmissionRate",
        label: "Transmission Rate",
        inputType: DYNAMIC_FORM_CONTROL_INPUT_TYPE_NUMBER,
        min: ElectricityRate.MIN_ELECTRICITY_RATE,
        max: ElectricityRate.MAX_ELECTRICITY_RATE,
        value: lEntity.mainTransmissionRate.toString(),
        placeholder: "Transmission Rate"
    }),
    new DynamicInputModel({
        id: "publicServiceRate",
        label: "Public Service Rate",
        inputType: DYNAMIC_FORM_CONTROL_INPUT_TYPE_NUMBER,
        min: ElectricityRate.MIN_ELECTRICITY_RATE,
        max: ElectricityRate.MAX_ELECTRICITY_RATE,
        value: lEntity.publicServiceRate.toString(),
        placeholder: "Public Service Rate"
    }),
    new DynamicInputModel({
        id: "validityStartDate",
        label: "Validity start date",
        inputType: DYNAMIC_FORM_CONTROL_INPUT_TYPE_DATE,
        maxLength: 10,
        value: MiscHelper.dateToDynamicInputDate(lEntity.validityStartDate),
        placeholder: "Validity start date"
    }),
    new DynamicInputModel({
        id: "validityEndDate",
        label: "Validity end date",
        inputType: DYNAMIC_FORM_CONTROL_INPUT_TYPE_DATE,
        value: MiscHelper.dateToDynamicInputDate(lEntity.validityEndDate),
        placeholder: "Validity end date"
    })
    ]
    return result

}

/**
* called by SelectableEntitiesListComponent->onInit
*
* @param aStrangeObject
*/
protected _getAddFormControlModel(aStrangeObject: Enumerate): DynamicFormControlModel[] {
    //console.debug('getAddFormControlModel->aStrangeObject:', aStrangeObject)
    let lEntity: Enumerate = new Enumerate().deserialize(aStrangeObject)
    console.debug('-----getAddFormControlModel->aStrangeObject, lEntity:', aStrangeObject, lEntity)
    //Add form fields
    const result: DynamicFormControlModel[] = this._getCommonFormControlModel(aStrangeObject)
    result.push(this._elTypeAddSelect.asDynamicInputModel())
    result.push(this._cityAddSelect.asDynamicInputModel())
    return result
}



/**
* Built onRowClicked
*
* @param anId
* @param aStrangeObject can be a row of dataTable
*/
protected _getEditFormControlModel(aStrangeObject: Enumerate): DynamicFormControlModel[] {
    console.log('getEditFormControlModel:', aStrangeObject)
    let result = this._getCommonFormControlModel(aStrangeObject)
    result = result.concat(DBEntity.getIdFormControlModel('id', aStrangeObject))
    result.push(this._elTypeEditSelect.asDynamicInputModel())
    result.push(this._cityEditSelect.asDynamicInputModel())
    // console.log('getEditFormControlModel:', result)

    return result
}

export abstract class SelectableEntitiesListComponent extends EntityListComponent {

public ngOnInit() {
    super.ngOnInit()
    this._setSelects()
}

/**
* redefine
*/
public onReloadClicked(anEvent) {
    super.onReloadClicked(anEvent)
    this._setSelects()
}


/**
* redefine
*/
public afterEntityUpdatedSucessful(){
    super.afterEntityUpdatedSucessful()
    this._setSelects()
}

/**
*
*/
protected abstract _setSelects()


}

export abstract class EntityListComponent extends ReloadableComponent implements AfterViewInit, OnInit {

protected _currentEntity: D = null // Set to null and not undefined cause of list.component.html tests for it  reason explained https://stackoverflow.com/questions/5076944/what-is-the-difference-between-null-and-undefined-in-javascript
protected abstract displayedColumnsArray: Array<string>; // Gives the order of the columns
public entitiesListTitle = this.constructor.name

// FORMS
entityAddFormGroup: FormGroup;
entityAddFormCtlModelArray: DynamicFormControlModel[];
entityEditFormGroup: FormGroup;
entityEditFormCtlModelArray: DynamicFormControlModel[];

// DATA TABLE variables
dataSource: SseEntityDataSource<D>;
selectionModel = new SelectionModel<D>(true, []);
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;

constructor(
    protected router: Router,
    public messageService: MessageService,
    protected logger: LoggerService,
    protected route: ActivatedRoute,
    protected entitiesService: SseEntityService<D>,
    protected formBuilder: FormBuilder,
    public formService: DynamicFormService,
    iconRegistry: MatIconRegistry,
    sanitizer: DomSanitizer,
    public entityPrototype: DBEntity,
    // private location: Location
) {
    super(
    iconRegistry,
    sanitizer,
    )
    if (entityPrototype === undefined || entityPrototype == null){
    throw new Error('constructor error, create me in the caller entityPrototype!')
    }
}

/**
* calls this._getAddFormControlModel() and adds it to entityAddFormCtlModelArray
*/
public ngOnInit() {
    // console.debug('ngOnInit called')
    if (this.entityPrototype === undefined){
    throw new Error('entity-list.component->ngOnInit-> this.entityPrototype is undefined, set it into constructor of descendant')
    }
    this.entitiesListTitle = StringHelper.camelCaseToSpaces(this.constructor.name.replace('Component', ''))


    this.dataSource = new SseEntityDataSource<D>(this.logger, this.entitiesService, this, this.entityPrototype);
    this.setMessageService();
    this.entityAddFormCtlModelArray = this._getAddFormControlModel(this.entityPrototype);
    this.entityAddFormGroup = this.formService.createFormGroup(this.entityAddFormCtlModelArray);

    this.dataSource.loadEntities()
}

protected abstract _getCommonFormControlModel(aStrangeObject: DBEntity): DynamicFormControlModel[]
protected abstract _getAddFormControlModel(aStrangeObject: DBEntity): DynamicFormControlModel[]

public ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
    this.dataSource.sort = this.sort;
//    this.cdr.detectChanges();
}

get currentEntity(): D {
    return this._currentEntity;
}

set currentEntity(value: D) {
    this._currentEntity = value;
    this.entitiesService.currentEntity = value;
}

/**
* Require dataSource not null
*/
public loadDatasourceWithPaginator() {
    // Init currentEntityId
    try {
    this.dataSource.loadEntities();
    } catch (e) {
    this.messageService.add(new UserMessage('Error loading entities', e, UserMessageType.Error));
    throw e;
    }
}

public applyFilter(filterValue: string) {
    filterValue = filterValue.trim(); // Remove whitespace
    filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches
    this.dataSource.filter = filterValue;
}


/**
* Require dataSource not null
*/
public setMessageService() {
    this.dataSource.messagesForUsers$.subscribe(
    usrMessage => {
        this.messageService.add(usrMessage);
    }
    );
}


abstract onRowClicked(row: any): void;

public buttonAddEntityClicked(dataValues: any) {
    console.debug('buttonAddEntityClicked-------->from Entitylist.components dataValues:', dataValues);
    let lEntity = this.entityPrototype.deserialize(dataValues, false)
    console.debug('buttonAddEntityClicked-------->from Entitylist.components lEntity:', lEntity);
    console.debug('buttonAddEntityClicked-------->from Entitylist.components lEntity.toJSON():', lEntity.toJSON());

    this.entitiesService.addEntityFromFormData(lEntity.toJSON()).subscribe(
    lData => {
        const msg = `Entity added successfully`;
        this.messageService.add(new UserMessage(msg, lData, UserMessageType.Info));
        this.afterEntityUpdatedSucessful()
    },
    lError => {
        const msg = `Entity add Error`;
        console.error('buttonAddEntityClicked->Error:', lError)
        this.messageService.add(new UserMessage(msg, lError, UserMessageType.Error));
        throw lError;
    }
    );
}

public afterEntityUpdatedSucessful(){
    this.loadDatasourceWithPaginator();
}


public buttonEditEntityClicked(jsonStringValues: string) {
    this.logger.debug('buttonAddEntityClicked-> from Entitylist.components:', jsonStringValues);
    let lEntity = this.entityPrototype.deserialize(jsonStringValues, false)
    this.logger.debug('buttonEditEntityClicked-> Entitylist.components: jsonStringValues, lEntity:', jsonStringValues, lEntity);

    this.entitiesService.updateEntityFromFormData(lEntity.toJSON()).subscribe(
    lData => {
        const msg = `Entity updated successfully`;
        this.messageService.add(new UserMessage(msg, lData, UserMessageType.Info));
        this.afterEntityUpdatedSucessful()
    },
    lError => {
        const msg = `Entity update Error`;
        console.error('buttonEditEntityClicked->Error:', lError)
        this.messageService.add(new UserMessage(msg, lError, UserMessageType.Error));
        throw lError;
    }
    );
}


public buttonRemoveSelectedRowsClicked() {
    let toReloadObservable: Observable<Object> = null;
    this.selectionModel.selected.forEach(item => {
    this.logger.debug('Deleting selected item:', item);
    toReloadObservable = this.entitiesService.deleteFromId(item.id);
    toReloadObservable.subscribe(
        data => {
        const msg = `Entity ${item.id} deleted successfully`;
        this.messageService.add(new UserMessage(msg, data, UserMessageType.Info));
        this.afterEntityUpdatedSucessful()
        },
        error => {
        const msg = `Error while deleting entity ${item.id}`;
        this.messageService.add(new UserMessage(msg, error, UserMessageType.Error));
        throw error;
        }
    );
    });
    this.selectionModel = new SelectionModel<D>(true, []);
    this._currentEntity = null;
    // When all are removed reload data source
}

public onReloadClicked(anEvent) {
    this.loadDatasourceWithPaginator();
}

public buttonMasterToggleClicked() {
    this.isAllSelected() ?
    this.selectionModel.clear() :
    this.dataSource.data.forEach(row => this.selectionModel.select(row));
}

public sampleAddButtonClicked() {
    Constants.SAMPLE_COMPANIES_JSON_DATA.forEach( (entity) => {
    // console.log('sampleAddButtonClicked', JSON.stringify(entity));
    this.buttonAddEntityClicked(entity);
    });
}

public isAllSelected() {
    const numSelected = this.selectionModel.selected.length;
    const numRows = this.dataSource.entitiesCount();
    return numSelected === numRows;
}

protected _updateEditFormFields(toUpdate: any) {
    console.log("updateEditFormFields->toUpdate, model", toUpdate, this.entityEditFormCtlModelArray);
    Object.entries(toUpdate).forEach(([key, value]) => {
    // console.log('updateEditFormFields->setting key', key, 'value:', value);
    const inputModel = this.formService.findById(key, this.entityEditFormCtlModelArray) as DynamicInputModel;

    if (inputModel == null) {
        throw new Error('updateEditFormFields->InputModel is null, key ' + key + ' not found into entityEditFormCtlModel val:' + value );
    }
    inputModel.valueUpdates.next(value as string)//If not reloading recreate the formGroup with this.entityAddFormGroup = this.formService.createFormGroup(this.entityAddFormCtlModelArray);
    // inputModel.valueUpdates.subscribe(value => console.log('new value assigned to field: ', newVal));
    // inputModel.disabledUpdates.next(true);
    });
}


}

Highly related to this post

Reason Behind The Error:

I don't think this error is related to mat-tab. This error is usually related to the initial development phase where there are uses of lifecycle hooks like ngAfterViewInit . Quoting directly from Angular Blog-

This type of error usually shows up beyond the initial development stages, when we start to have some more expressions in our templates, and we have typically started to use some of the lifecycle hooks like AfterViewInit.

You can't use paginator reference in ngAfterViewInit() and modify the Data Source immediately, because that will trigger a further modification of the data but Angular view generation process is not finished yet, so its not clear if the value of the variable that you are using as an expression in the template should modified the one or the previous one.

Possible Solution:

In order to solve this issue, we need to let Angular first display the data with the loading flag set to false.

So, a possible solution is to use setTimeOut or delay(0) before sorting the datasource in ngAfterViewInit() .

Reason Why This Solution Works:

  • The initial value of the flag is false, and so the loading indicator will NOT be displayed initially.
  • ngAfterViewInit() gets called, but the data source is not immediately called, so no modifications of the loading indicator will be made synchronously via ngAfterViewInit() .

  • Angular then finishes rendering the view and reflects the latest data changes on the screen, and the Javascript VM turn completes.

  • One moment later, the setTimeout() call (also used inside delay(0) ) is triggered, and only then the data source loads its data.
  • the loading flag is set to true, and the loading indicator will now be displayed.
  • Angular finishes rendering the view, and reflects the latest changes on the screen, which causes the loading indicator to get displayed.

Resources:

To understand the issue more deeply, please have a look at this documentation from where I have quoted. Here the overall scenario is explained with example.

You can also have a look at this answer, where using ngAfterContentInit in place of ngAfterViewInit is listed as another possible solution.

I hope this would help.

UPDATE:

Alternative Solutions:

As @jo_va mentioned in the comment, there are other possible solutions to this problem.

  1. In place of using setTimeOut() , changeDetector.detectChanges() can also be used.

    Here I am directly explaining from @jo_va's suggestion:

    It could be interesting to mention changeDetector.detectChanges() where changeDector is an injected ChangeDetectorRef. This is another widely used solution to this problem, and I think cleaner than setTimeout.

  2. And also Promise.resolve might be another alternative to setTimeout .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM