diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index 5acbe3e..044102d 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); @@ -378,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/package.json b/package.json index 210d11d..33c704c 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/create-form.ts b/projects/ngx-sub-form/src/lib/create-form.ts index 2f4ce39..9e62932 100644 --- a/projects/ngx-sub-form/src/lib/create-form.ts +++ b/projects/ngx-sub-form/src/lib/create-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) @@ -234,12 +235,13 @@ export function createForm( ), setDisabledState$: setDisabledState$.pipe( tap((shouldDisable: boolean) => { - shouldDisable ? formGroup.disable({ emitEvent: false }) : formGroup.enable({ emitEvent: false }); + // 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 }); }), ), updateValue$: updateValueAndValidity$.pipe( tap(() => { - formGroup.updateValueAndValidity({ emitEvent: false }); + formGroup.updateValueAndValidity({ emitEvent: true }); }), ), bindTouched$: combineLatest([componentHooks.registerOnTouched$, options.touched$ ?? EMPTY]).pipe( 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 4472eb2..316cb32 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,33 @@ 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])); - } - } + formArray.clear(); + 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); + } + } + private formIsFormWithArrayControls(): this is NgxFormWithArrayControls { return typeof (this as unknown as NgxFormWithArrayControls).createFormArrayControl === 'function'; } @@ -423,10 +439,11 @@ export abstract class NgxSubFormComponent(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) => { @@ -161,14 +160,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) { 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 bfe7628..1c0bf3b 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; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8524868..01a8e5e 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 {} diff --git a/yarn.lock b/yarn.lock index 77984f1..4c4086a 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"