diff --git a/README.md b/README.md index b7cc5f55..a06bc7e7 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 diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index 405687fa..d71d4d04 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 185cf27b..40a7dd09 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/projects/ngx-sub-form/src/lib/create-form.ts b/projects/ngx-sub-form/src/lib/create-form.ts index 90097533..b87ad296 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 = merge(formGroup.valueChanges, transformedValue$).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,14 @@ export function createForm( delay(0), tap(([onTouched]) => onTouched()), ), + isEqual$: isEqual$.pipe( + delay(0), + 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 ed51a4c2..dcb8ded8 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; + // Returns true 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 2f664855..2d60e7cb 100644 --- a/src/app/main/listing/listing-form/listing-form.component.html +++ b/src/app/main/listing/listing-form/listing-form.component.html @@ -124,11 +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
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 c7803873..e716940b 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,7 @@ export class ListingFormComponent { input$: this.input$, output$: this.listingUpdated, manualSave$: this.manualSave$$, + isEqual$: this.isEqual$$, formControls: { vehicleProduct: new UntypedFormControl(null), droidProduct: new UntypedFormControl(null),