From 907eb61da7ab618db36aa63566cb30edeec0830a Mon Sep 17 00:00:00 2001 From: Andy Schmidt Date: Mon, 20 Jun 2022 17:22:52 +0200 Subject: [PATCH 1/6] 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 c178429f211029fa5485edd76544ff9b0b29fd83 Mon Sep 17 00:00:00 2001 From: Andy Schmidt Date: Fri, 24 Jun 2022 15:50:24 +0200 Subject: [PATCH 2/6] Added option to emit raw value if form is disabled --- projects/ngx-sub-form/src/lib/ngx-sub-form.ts | 1 + projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts | 1 + 2 files changed, 2 insertions(+) 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..bd291320 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form.ts @@ -182,6 +182,7 @@ export function createForm( ); } }), + map(value => (!isNullOrUndefined(options.emitRawValue) && options.emitRawValue) ? formGroup.getRawValue() : value), map(value => options.fromFormGroup ? options.fromFormGroup(value) diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts index bfe76283..1c0bf3b3 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts @@ -74,6 +74,7 @@ export type NgxSubFormOptions; formGroupOptions?: FormGroupOptions; emitNullOnDestroy?: boolean; + emitRawValue?: boolean; componentHooks?: ComponentHooks; // emit on this observable to mark the control as touched touched$?: Observable; From 078edef6155e61809fbebacc1bf652bd07183ad2 Mon Sep 17 00:00:00 2001 From: Andy Schmidt Date: Fri, 24 Jun 2022 15:51:46 +0200 Subject: [PATCH 3/6] 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 4/6] 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 5/6] 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 Date: Tue, 26 Jul 2022 09:58:45 +0200 Subject: [PATCH 6/6] Fixed to clone default value --- package.json | 2 ++ projects/ngx-sub-form/src/lib/helpers.ts | 5 ++--- yarn.lock | 10 ++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 210d11d0..33c704c5 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@types/uuid": "8.3.4", "commitizen": "4.2.4", "core-js": "3.23.1", + "lodash-es": "4.17.21", "fast-deep-equal": "3.1.3", "ngx-observable-lifecycle": "2.2.1", "rxjs": "7.5.5", @@ -70,6 +71,7 @@ "@types/jasmine": "4.0.3", "@types/jasminewd2": "2.0.10", "@types/node": "17.0.40", + "@types/lodash": "4.14.182", "@typescript-eslint/eslint-plugin": "5.28.0", "@typescript-eslint/parser": "5.28.0", "cypress": "10.1.0", diff --git a/projects/ngx-sub-form/src/lib/helpers.ts b/projects/ngx-sub-form/src/lib/helpers.ts index e1f90b35..d54e3c7f 100644 --- a/projects/ngx-sub-form/src/lib/helpers.ts +++ b/projects/ngx-sub-form/src/lib/helpers.ts @@ -20,8 +20,7 @@ import { OneOfControlsTypes, TypedFormGroup, } from './shared/ngx-sub-form-utils'; - -export const deepCopy = (value: T): T => JSON.parse(JSON.stringify(value)); +import { cloneDeep } from 'lodash'; /** @internal */ export const patchClassInstance = (componentInstance: any, obj: Object) => { @@ -122,7 +121,7 @@ export function createFormDataFromOptions( options.formControls, options.formGroupOptions as AbstractControlOptions, ) as TypedFormGroup; - const defaultValues: FormInterface = deepCopy(formGroup.value); + const defaultValues: FormInterface = cloneDeep(formGroup.value); const formGroupKeys: (keyof FormInterface)[] = Object.keys(defaultValues) as (keyof FormInterface)[]; const formControlNames: ControlsNames = formGroupKeys.reduce>( (acc, curr) => { diff --git a/yarn.lock b/yarn.lock index 77984f19..4c4086a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2186,6 +2186,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@4.14.182": + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -6901,6 +6906,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash-es@4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.capitalize@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9"