From 907eb61da7ab618db36aa63566cb30edeec0830a Mon Sep 17 00:00:00 2001 From: Andy Schmidt Date: Mon, 20 Jun 2022 17:22:52 +0200 Subject: [PATCH 1/4] Fixed to propagate formarray validation status --- cypress/e2e/app.cy.ts | 97 ++++++------------- .../lib/deprecated/ngx-sub-form.component.ts | 56 ++++++++--- projects/ngx-sub-form/src/lib/ngx-sub-form.ts | 2 +- src/app/app.module.ts | 8 +- 4 files changed, 80 insertions(+), 83 deletions(-) diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index 5acbe3e3..36264ec0 100644 --- a/cypress/e2e/app.cy.ts +++ b/cypress/e2e/app.cy.ts @@ -174,6 +174,9 @@ context(`EJawa demo`, () => { }, crewMembers: { required: true, + crewMembers: { + minimumCrewMemberCount: 2 + }, }, wingCount: { required: true, @@ -194,80 +197,42 @@ context(`EJawa demo`, () => { DOM.form.elements.vehicleForm.addCrewMemberButton.click(); - if (id === 'old') { - DOM.form.errors.should($el => { - expect(extractErrors($el)).to.eql({ - vehicleProduct: { - spaceship: { - color: { - required: true, - }, - crewMembers: { - crewMembers: [ - { - firstName: { - required: true, - }, - lastName: { - required: true, - }, - }, - ], - }, - wingCount: { - required: true, - }, + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + spaceship: { + color: { + required: true, }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); - }); - } else { - DOM.form.errors.should($el => { - expect(extractErrors($el)).to.eql({ - vehicleProduct: { - spaceship: { - color: { - required: true, - }, + crewMembers: { crewMembers: { - crewMembers: { - minimumCrewMemberCount: 2, - 0: { - firstName: { - required: true, - }, - lastName: { - required: true, - }, + minimumCrewMemberCount: 2, + 0: { + firstName: { + required: true, + }, + lastName: { + required: true, }, }, }, - wingCount: { - required: true, - }, + }, + wingCount: { + required: true, }, }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, }); - } + }); DOM.form.elements.selectListingTypeByType(ListingType.DROID); diff --git a/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts b/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts index 4472eb20..fb5ff605 100644 --- a/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts +++ b/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts @@ -173,9 +173,20 @@ export abstract class NgxSubFormComponent 0 && values.some(x => !isNullOrUndefined(x))) { - controls[key] = values; + value = { + ...value, + ...values + } } + controls[key] = value; } else if (control && filterControl(control, key, false)) { controls[key] = mapControl(control, key); } @@ -287,28 +298,45 @@ export abstract class NgxSubFormComponent { - if (this.formGroup.get(key) instanceof UntypedFormArray && Array.isArray(value)) { + if ( + this.formGroup.get(key) instanceof UntypedFormArray && + Array.isArray(value) + ) { const formArray: UntypedFormArray = this.formGroup.get(key) as UntypedFormArray; - // instead of creating a new array every time and push a new FormControl // we just remove or add what is necessary so that: // - it is as efficient as possible and do not create unnecessary FormControl every time // - validators are not destroyed/created again and eventually fire again for no reason - while (formArray.length > value.length) { - formArray.removeAt(formArray.length - 1); - } - - for (let i = formArray.length; i < value.length; i++) { - if (this.formIsFormWithArrayControls()) { - formArray.insert(i, this.createFormArrayControl(key as ArrayPropertyKey, value[i])); - } else { - formArray.insert(i, new UntypedFormControl(value[i])); - } - } + this.removeUnnecassaryObjects(formArray, value); + this.addAdditionalObjects(formArray, value, key); } }); } + private addAdditionalObjects( + formArray: UntypedFormArray, + value: Array, + key: string + ) { + for (let i = formArray.length; i < value.length; i++) { + const control = this.formIsFormWithArrayControls() + ? this.createFormArrayControl( + key as ArrayPropertyKey, + value[i] + ) + : new UntypedFormControl(value[i]); + formArray.insert(i, control, { emitEvent: this.emitInitialValueOnInit }); + } + } + + private removeUnnecassaryObjects(formArray: UntypedFormArray, value: Array) { + while (formArray.length > value.length) { + formArray.removeAt(formArray.length - 1, { + emitEvent: this.emitInitialValueOnInit, + }); + } + } + private formIsFormWithArrayControls(): this is NgxFormWithArrayControls { return typeof (this as unknown as NgxFormWithArrayControls).createFormArrayControl === 'function'; } diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form.ts index 2f4ce39a..4c6546c8 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form.ts @@ -239,7 +239,7 @@ export function createForm( ), updateValue$: updateValueAndValidity$.pipe( tap(() => { - formGroup.updateValueAndValidity({ emitEvent: false }); + formGroup.updateValueAndValidity({ emitEvent: true }); }), ), bindTouched$: combineLatest([componentHooks.registerOnTouched$, options.touched$ ?? EMPTY]).pipe( diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 85248689..01a8e5ec 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,9 +1,13 @@ -import { NgModule } from '@angular/core'; +import { registerLocaleData } from '@angular/common'; +import { LOCALE_ID, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { SharedModule } from './shared/shared.module'; +import localeDe from '@angular/common/locales/de'; + +registerLocaleData(localeDe, 'de'); @NgModule({ declarations: [AppComponent], @@ -25,7 +29,7 @@ import { SharedModule } from './shared/shared.module'; ), SharedModule, ], - providers: [], + providers: [{provide: LOCALE_ID, useValue: 'de' }], bootstrap: [AppComponent], }) export class AppModule {} From 078edef6155e61809fbebacc1bf652bd07183ad2 Mon Sep 17 00:00:00 2001 From: Andy Schmidt Date: Fri, 24 Jun 2022 15:51:46 +0200 Subject: [PATCH 2/4] Set emit value to true if form is disabled and enabled to emit validation status --- projects/ngx-sub-form/src/lib/ngx-sub-form.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form.ts index 4c6546c8..278f90ca 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form.ts @@ -234,7 +234,7 @@ export function createForm( ), setDisabledState$: setDisabledState$.pipe( tap((shouldDisable: boolean) => { - shouldDisable ? formGroup.disable({ emitEvent: false }) : formGroup.enable({ emitEvent: false }); + shouldDisable ? formGroup.disable({ emitEvent: true }) : formGroup.enable({ emitEvent: true }); }), ), updateValue$: updateValueAndValidity$.pipe( From 8eaf1799d301ddc54f600c574a72118f5ba3a3bc Mon Sep 17 00:00:00 2001 From: Andy Schmidt Date: Mon, 27 Jun 2022 13:16:07 +0200 Subject: [PATCH 3/4] Clear formarray --- .../src/lib/deprecated/ngx-sub-form.component.ts | 16 ++-------------- projects/ngx-sub-form/src/lib/helpers.ts | 9 +-------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts b/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts index fb5ff605..1505b356 100644 --- a/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts +++ b/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts @@ -303,11 +303,7 @@ export abstract class NgxSubFormComponent) { - while (formArray.length > value.length) { - formArray.removeAt(formArray.length - 1, { - emitEvent: this.emitInitialValueOnInit, - }); + formArray.insert(i, control); } } diff --git a/projects/ngx-sub-form/src/lib/helpers.ts b/projects/ngx-sub-form/src/lib/helpers.ts index 27521656..e1f90b35 100644 --- a/projects/ngx-sub-form/src/lib/helpers.ts +++ b/projects/ngx-sub-form/src/lib/helpers.ts @@ -161,14 +161,7 @@ export const handleFormArrays = ( return; } - // instead of creating a new array every time and push a new FormControl - // we just remove or add what is necessary so that: - // - it is as efficient as possible and do not create unnecessary FormControl every time - // - validators are not destroyed/created again and eventually fire again for no reason - while (control.length > value.length) { - control.removeAt(control.length - 1); - } - + control.clear(); for (let i = control.length; i < value.length; i++) { const newControl = createFormArrayControl(key as ArrayPropertyKey, value[i]); if (control.disabled) { From 9fd91e84f4d025a95cf8208aad4a1cccb4448bc9 Mon Sep 17 00:00:00 2001 From: Andy Schmidt Date: Mon, 27 Jun 2022 14:46:43 +0200 Subject: [PATCH 4/4] Created e2e test for error validation after enbable and disable form --- cypress/e2e/app.cy.ts | 59 +++++++++++++++++++ projects/ngx-sub-form/src/lib/create-form.ts | 1 + .../lib/deprecated/ngx-sub-form.component.ts | 5 +- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index 36264ec0..044102de 100644 --- a/cypress/e2e/app.cy.ts +++ b/cypress/e2e/app.cy.ts @@ -343,6 +343,65 @@ context(`EJawa demo`, () => { }); }); }); + + it(`should display the (nested) errors from the form after enable/disable`, () => { + DOM.createNewButton.click(); + + DOM.form.elements.selectListingTypeByType(ListingType.VEHICLE); + + DOM.form.elements.vehicleForm.selectVehicleTypeByType(VehicleType.SPACESHIP); + + DOM.form.elements.vehicleForm.addCrewMemberButton.click(); + + const errorsToExpect = { + vehicleProduct: { + spaceship: { + color: { + required: true, + }, + crewMembers: { + crewMembers: { + minimumCrewMemberCount: 2, + 0: { + firstName: { + required: true, + }, + lastName: { + required: true, + }, + }, + }, + }, + wingCount: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }; + + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql(errorsToExpect); + }); + + DOM.readonlyToggle.click(); + + DOM.form.errors.should('not.exist'); + + DOM.readonlyToggle.click(); + + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql(errorsToExpect); + }); + }); }); }); }); diff --git a/projects/ngx-sub-form/src/lib/create-form.ts b/projects/ngx-sub-form/src/lib/create-form.ts index 278f90ca..a0ff2da5 100644 --- a/projects/ngx-sub-form/src/lib/create-form.ts +++ b/projects/ngx-sub-form/src/lib/create-form.ts @@ -234,6 +234,7 @@ export function createForm( ), setDisabledState$: setDisabledState$.pipe( tap((shouldDisable: boolean) => { + // We have to emit to update and validate the value and propagate it to the parent shouldDisable ? formGroup.disable({ emitEvent: true }) : formGroup.enable({ emitEvent: true }); }), ), diff --git a/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts b/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts index 1505b356..316cb32b 100644 --- a/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts +++ b/projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts @@ -439,10 +439,11 @@ export abstract class NgxSubFormComponent