Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to emit raw value if form is disabled #265

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
156 changes: 90 additions & 66 deletions cypress/e2e/app.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ context(`EJawa demo`, () => {
},
crewMembers: {
required: true,
crewMembers: {
minimumCrewMemberCount: 2
},
},
wingCount: {
required: true,
Expand All @@ -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);

Expand Down Expand Up @@ -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);
});
});
});
});
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions projects/ngx-sub-form/src/lib/create-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export function createForm<ControlInterface, FormInterface>(
);
}
}),
map(value => (!isNullOrUndefined(options.emitRawValue) && options.emitRawValue) ? formGroup.getRawValue() : value),
map(value =>
options.fromFormGroup
? options.fromFormGroup(value)
Expand Down Expand Up @@ -234,12 +235,13 @@ export function createForm<ControlInterface, FormInterface>(
),
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(
Expand Down
57 changes: 37 additions & 20 deletions projects/ngx-sub-form/src/lib/deprecated/ngx-sub-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,20 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
}
}

let value = undefined;
if (control && filterControl(control, key, false)) {
value = {
...mapControl(control, key)
};
}

if (values.length > 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);
}
Expand Down Expand Up @@ -287,28 +298,33 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont

private handleFormArrayControls(obj: any) {
Object.entries(obj).forEach(([key, value]) => {
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<FormInterface>, value[i]));
} else {
formArray.insert(i, new UntypedFormControl(value[i]));
}
}
formArray.clear();
this.addAdditionalObjects(formArray, value, key);
}
});
}

private addAdditionalObjects(
formArray: UntypedFormArray,
value: Array<any>,
key: string
) {
for (let i = formArray.length; i < value.length; i++) {
const control = this.formIsFormWithArrayControls()
? this.createFormArrayControl(
key as ArrayPropertyKey<FormInterface>,
value[i]
)
: new UntypedFormControl(value[i]);
formArray.insert(i, control);
}
}

private formIsFormWithArrayControls(): this is NgxFormWithArrayControls<FormInterface> {
return typeof (this as unknown as NgxFormWithArrayControls<FormInterface>).createFormArrayControl === 'function';
}
Expand Down Expand Up @@ -423,10 +439,11 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
return;
}

// We have to emit to update and validate the value and propagate it to the parent
if (shouldDisable) {
this.formGroup.disable({ emitEvent: false });
this.formGroup.disable({ emitEvent: true });
} else {
this.formGroup.enable({ emitEvent: false });
this.formGroup.enable({ emitEvent: true });
}
}
}
Expand Down
14 changes: 3 additions & 11 deletions projects/ngx-sub-form/src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import {
OneOfControlsTypes,
TypedFormGroup,
} from './shared/ngx-sub-form-utils';

export const deepCopy = <T>(value: T): T => JSON.parse(JSON.stringify(value));
import { cloneDeep } from 'lodash';

/** @internal */
export const patchClassInstance = (componentInstance: any, obj: Object) => {
Expand Down Expand Up @@ -122,7 +121,7 @@ export function createFormDataFromOptions<ControlInterface, FormInterface>(
options.formControls,
options.formGroupOptions as AbstractControlOptions,
) as TypedFormGroup<FormInterface>;
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<FormInterface> = formGroupKeys.reduce<ControlsNames<FormInterface>>(
(acc, curr) => {
Expand Down Expand Up @@ -161,14 +160,7 @@ export const handleFormArrays = <FormInterface>(
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<FormInterface>, value[i]);
if (control.disabled) {
Expand Down
1 change: 1 addition & 0 deletions projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export type NgxSubFormOptions<ControlInterface, FormInterface = ControlInterface
formControls: Controls<FormInterface>;
formGroupOptions?: FormGroupOptions<FormInterface>;
emitNullOnDestroy?: boolean;
emitRawValue?: boolean;
componentHooks?: ComponentHooks;
// emit on this observable to mark the control as touched
touched$?: Observable<void>;
Expand Down
8 changes: 6 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand All @@ -25,7 +29,7 @@ import { SharedModule } from './shared/shared.module';
),
SharedModule,
],
providers: [],
providers: [{provide: LOCALE_ID, useValue: 'de' }],
bootstrap: [AppComponent],
})
export class AppModule {}
Loading