From fec6a12b396fa73a5e298810f9d98db5e262d95f Mon Sep 17 00:00:00 2001 From: SvenA Date: Tue, 3 Jan 2023 16:49:55 +0100 Subject: [PATCH 1/4] feat: optionally expose isEqual for root-form --- projects/ngx-sub-form/src/lib/create-form.ts | 19 +++++++++++++++++++ .../src/lib/ngx-sub-form.types.ts | 2 ++ .../listing-form/listing-form.component.html | 1 + .../listing-form/listing-form.component.ts | 5 ++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/projects/ngx-sub-form/src/lib/create-form.ts b/projects/ngx-sub-form/src/lib/create-form.ts index 9009753..5c20638 100644 --- a/projects/ngx-sub-form/src/lib/create-form.ts +++ b/projects/ngx-sub-form/src/lib/create-form.ts @@ -202,6 +202,18 @@ export function createForm( broadcastValueToParent$, ).pipe(shareReplay({ bufferSize: 1, refCount: true })); + const isEqual$: Observable = formGroup.valueChanges.pipe( + startWith(formGroup.value), + withLatestFrom(transformedValue$), + map(([value, transformedValue]) => { + if (!isRoot(options)) { + return true; + } else { + return options.isEqual$ ? isEqual(value, transformedValue) : true; + } + }), + ); + const emitNullOnDestroy$: Observable = // emit null when destroyed by default isNullOrUndefined(options.emitNullOnDestroy) || options.emitNullOnDestroy @@ -247,6 +259,13 @@ export function createForm( delay(0), tap(([onTouched]) => onTouched()), ), + isEqual$: isEqual$.pipe( + tap(value => { + if (isRoot(options)) { + options.isEqual$?.next(value); + } + }), + ), }; merge(...Object.values(sideEffects)) 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 ed51a4c..c11e7f6 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 @@ -102,6 +102,8 @@ export type NgxRootFormOptions< // if you want to control how frequently the form emits on the output$, you can customise the emission rate with this // option. e.g. `handleEmissionRate: formValue$ => formValue$.pipe(debounceTime(300)),` handleEmissionRate?: (obs$: Observable) => Observable; + // Exposes if the transformed value of input$ equals the current value of the form + isEqual$?: Subject; }; export enum FormType { diff --git a/src/app/main/listing/listing-form/listing-form.component.html b/src/app/main/listing/listing-form/listing-form.component.html index 2f66485..43d8993 100644 --- a/src/app/main/listing/listing-form/listing-form.component.html +++ b/src/app/main/listing/listing-form/listing-form.component.html @@ -129,6 +129,7 @@
Form is invalid
+
Form value is equal
diff --git a/src/app/main/listing/listing-form/listing-form.component.ts b/src/app/main/listing/listing-form/listing-form.component.ts index c780387..54442fd 100644 --- a/src/app/main/listing/listing-form/listing-form.component.ts +++ b/src/app/main/listing/listing-form/listing-form.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, Input, Output } from '@angular/core'; import { UntypedFormControl, Validators } from '@angular/forms'; import { createForm, FormType } from 'ngx-sub-form'; -import { Subject } from 'rxjs'; +import { Subject, tap } from 'rxjs'; import { ListingType, OneListing } from 'src/app/interfaces/listing.interface'; import { OneDroid } from '../../../interfaces/droid.interface'; import { OneVehicle } from '../../../interfaces/vehicle.interface'; @@ -39,6 +39,7 @@ export class ListingFormComponent { @Output() listingUpdated: Subject = new Subject(); public manualSave$$: Subject = new Subject(); + public isEqual$$: Subject = new Subject(); public form = createForm(this, { formType: FormType.ROOT, @@ -46,6 +47,8 @@ export class ListingFormComponent { input$: this.input$, output$: this.listingUpdated, manualSave$: this.manualSave$$, + isEqual$: this.isEqual$$, + outputFilterPredicate: () => true, formControls: { vehicleProduct: new UntypedFormControl(null), droidProduct: new UntypedFormControl(null), From a3b63c1073ae1e9f093c15abc0a7577900ebe0d3 Mon Sep 17 00:00:00 2001 From: SvenA Date: Wed, 4 Jan 2023 11:14:55 +0100 Subject: [PATCH 2/4] fix: update isEqual after the initial form value has changed --- projects/ngx-sub-form/src/lib/create-form.ts | 3 ++- projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/ngx-sub-form/src/lib/create-form.ts b/projects/ngx-sub-form/src/lib/create-form.ts index 5c20638..b87ad29 100644 --- a/projects/ngx-sub-form/src/lib/create-form.ts +++ b/projects/ngx-sub-form/src/lib/create-form.ts @@ -202,7 +202,7 @@ export function createForm( broadcastValueToParent$, ).pipe(shareReplay({ bufferSize: 1, refCount: true })); - const isEqual$: Observable = formGroup.valueChanges.pipe( + const isEqual$: Observable = merge(formGroup.valueChanges, transformedValue$).pipe( startWith(formGroup.value), withLatestFrom(transformedValue$), map(([value, transformedValue]) => { @@ -260,6 +260,7 @@ export function createForm( tap(([onTouched]) => onTouched()), ), isEqual$: isEqual$.pipe( + delay(0), tap(value => { if (isRoot(options)) { options.isEqual$?.next(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 c11e7f6..dcb8ded 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 @@ -102,7 +102,7 @@ export type NgxRootFormOptions< // if you want to control how frequently the form emits on the output$, you can customise the emission rate with this // option. e.g. `handleEmissionRate: formValue$ => formValue$.pipe(debounceTime(300)),` handleEmissionRate?: (obs$: Observable) => Observable; - // Exposes if the transformed value of input$ equals the current value of the form + // Returns true if the transformed value of input$ equals the current value of the form isEqual$?: Subject; }; From bce454d21a8a104fe7f086e9e8af19423f251f1e Mon Sep 17 00:00:00 2001 From: SvenA Date: Wed, 4 Jan 2023 11:17:14 +0100 Subject: [PATCH 3/4] test: isEqual should be tested through the E2E --- cypress/e2e/app.cy.ts | 22 ++++++++++++++++++- cypress/helpers/dom.helper.ts | 6 +++++ .../listing-form/listing-form.component.html | 3 ++- .../listing-form/listing-form.component.ts | 1 - 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index 405687f..d71d4d0 100644 --- a/cypress/e2e/app.cy.ts +++ b/cypress/e2e/app.cy.ts @@ -2,7 +2,7 @@ import { extractErrors, FormElement, hardcodedElementsToTestList } from '../../c import { DOM, getFormList, getFormValue } from '../../cypress/helpers/dom.helper'; import { DroidType } from '../../src/app/interfaces/droid.interface'; import { ListingType, VehicleListing } from '../../src/app/interfaces/listing.interface'; -import { Spaceship, VehicleType } from '../../src/app/interfaces/vehicle.interface'; +import { Spaceship, Speeder, VehicleType } from '../../src/app/interfaces/vehicle.interface'; import { hardCodedListings } from '../../src/app/services/listings.data'; context(`EJawa demo`, () => { @@ -333,4 +333,24 @@ context(`EJawa demo`, () => { }); }); }); + + it(`should display is equal when the form value is equal to the initial value of the form`, () => { + // Check initial value after selecting a list item + DOM.list.elements.cy.eq(0).click(); + DOM.form.isEqual.should('exist'); + + // Should not show equal when a value in the form has changed + DOM.form.elements.price.clear().type('1'); + DOM.form.isEqual.should('not.exist'); + + // Should show equal when all values in the form are equal to the initial value + DOM.form.elements.price.clear().type(hardCodedListings[0].price.toString()); + DOM.form.isEqual.should('exist'); + + // Should show equal after changing values and submitting the form, as the changed values now form the new initial value + DOM.form.elements.price.clear().type('1'); + DOM.form.isEqual.should('not.exist'); + DOM.form.upsertButton.click(); + DOM.form.isEqual.should('exist'); + }); }); diff --git a/cypress/helpers/dom.helper.ts b/cypress/helpers/dom.helper.ts index 185cf27..40a7dd0 100644 --- a/cypress/helpers/dom.helper.ts +++ b/cypress/helpers/dom.helper.ts @@ -51,6 +51,9 @@ export const DOM = { get cy() { return cy.get('app-listing'); }, + get isEqual() { + return cy.get(`*[data-is-equal]`); + }, get errors() { return cy.get(`*[data-errors]`); }, @@ -111,6 +114,9 @@ export const DOM = { }, }; }, + get upsertButton() { + return cy.get('*[data-upsert-button]'); + }, }; }, }; diff --git a/src/app/main/listing/listing-form/listing-form.component.html b/src/app/main/listing/listing-form/listing-form.component.html index 43d8993..2d60e7c 100644 --- a/src/app/main/listing/listing-form/listing-form.component.html +++ b/src/app/main/listing/listing-form/listing-form.component.html @@ -124,12 +124,13 @@ color="primary" (click)="manualSave$$.next()" [disabled]="form.formGroup.invalid || form.formGroup.disabled" + data-upsert-button > Upsert
Form is invalid
-
Form value is equal
+
Form value is equal
diff --git a/src/app/main/listing/listing-form/listing-form.component.ts b/src/app/main/listing/listing-form/listing-form.component.ts index 54442fd..e716940 100644 --- a/src/app/main/listing/listing-form/listing-form.component.ts +++ b/src/app/main/listing/listing-form/listing-form.component.ts @@ -48,7 +48,6 @@ export class ListingFormComponent { output$: this.listingUpdated, manualSave$: this.manualSave$$, isEqual$: this.isEqual$$, - outputFilterPredicate: () => true, formControls: { vehicleProduct: new UntypedFormControl(null), droidProduct: new UntypedFormControl(null), From 206c7e8567dda41575b58712e42a43e803a1ac82 Mon Sep 17 00:00:00 2001 From: SvenA Date: Wed, 4 Jan 2023 11:18:08 +0100 Subject: [PATCH 4/4] docs: update readme with new isEqual property --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b7cc5f5..a06bc7e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ npm i ngx-sub-form | `13.x` | `5.2.0` (non breaking but new API available as well) | | `14.x` | `6.0.0` (Angular 14 upgrade only) | | `14.x` | `7.0.0` (deprecated API is now removed) | -| `15.x` | `8.0.0` | +| `15.x` | `8.x.x` | # API @@ -70,6 +70,7 @@ This function takes as parameter a configuration object and returns an object re | `manualSave$` | `Observable` | Optional | ✅ | ❌ | By default a root form will automatically broadcast all the form updates (through the `output$`) as soon as there's a change. If you wish to "save" the form only when you click on a save button for example, you can create a subject on your side and pass it here. Whenever you call `next` on your subject, assuming the form is valid, it'll broadcast te form value to the parent (through the `output$`) | | `outputFilterPredicate` | `(currentInputValue: FormInterface, outputValue: FormInterface) => boolean` | Optional | ✅ | ❌ | The default behaviour is to compare the current transformed value of `input$` with the current value of the form _(deep check)_, and if these are equal, the value won't be passed to `output$` in order to prevent the broadcast | | `handleEmissionRate` | `(obs$: Observable) => Observable` | Optional | ✅ | ❌ | If you want to control how frequently the form emits on the `output$`, you can customise the emission rate with this. Example: `handleEmissionRate: formValue$ => formValue$.pipe(debounceTime(300))` | +| `isEqual$` | `Subject` | Optional | ✅ | ❌ | When this subject emits `true`, the current form value is equal to the initial form value | # Principles