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

Feat/expose is equal #290

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -70,6 +70,7 @@ This function takes as parameter a configuration object and returns an object re
| `manualSave$` | `Observable<void>` | 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<FormInterface>) => Observable<FormInterface>` | 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<boolean>` | Optional | ✅ | ❌ | When this subject emits `true`, the current form value is equal to the initial form value |

# Principles

Expand Down
22 changes: 21 additions & 1 deletion cypress/e2e/app.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`, () => {
Expand Down Expand Up @@ -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');
});
});
6 changes: 6 additions & 0 deletions cypress/helpers/dom.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]`);
},
Expand Down Expand Up @@ -111,6 +114,9 @@ export const DOM = {
},
};
},
get upsertButton() {
return cy.get('*[data-upsert-button]');
},
};
},
};
Expand Down
20 changes: 20 additions & 0 deletions projects/ngx-sub-form/src/lib/create-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ export function createForm<ControlInterface, FormInterface extends {}>(
broadcastValueToParent$,
).pipe(shareReplay({ bufferSize: 1, refCount: true }));

const isEqual$: Observable<boolean> = merge(formGroup.valueChanges, transformedValue$).pipe(
startWith(formGroup.value),
withLatestFrom(transformedValue$),
map(([value, transformedValue]) => {
if (!isRoot<ControlInterface, FormInterface>(options)) {
return true;
} else {
return options.isEqual$ ? isEqual(value, transformedValue) : true;
}
}),
);

const emitNullOnDestroy$: Observable<null> =
// emit null when destroyed by default
isNullOrUndefined(options.emitNullOnDestroy) || options.emitNullOnDestroy
Expand Down Expand Up @@ -247,6 +259,14 @@ export function createForm<ControlInterface, FormInterface extends {}>(
delay(0),
tap(([onTouched]) => onTouched()),
),
isEqual$: isEqual$.pipe(
delay(0),
tap(value => {
if (isRoot<ControlInterface, FormInterface>(options)) {
options.isEqual$?.next(value);
}
}),
),
};

merge(...Object.values(sideEffects))
Expand Down
2 changes: 2 additions & 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 @@ -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<FormInterface>) => Observable<FormInterface>;
// Returns true if the transformed value of input$ equals the current value of the form
isEqual$?: Subject<boolean>;
};

export enum FormType {
Expand Down
2 changes: 2 additions & 0 deletions src/app/main/listing/listing-form/listing-form.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@
color="primary"
(click)="manualSave$$.next()"
[disabled]="form.formGroup.invalid || form.formGroup.disabled"
data-upsert-button
>
Upsert
</button>

<div *ngIf="form.formGroup.invalid" class="invalid-form">Form is invalid</div>
<div *ngIf="isEqual$$ | async" class="invalid-form" data-is-equal>Form value is equal</div>
</div>
</mat-card-actions>
</mat-card>
Expand Down
4 changes: 3 additions & 1 deletion src/app/main/listing/listing-form/listing-form.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -39,13 +39,15 @@ export class ListingFormComponent {
@Output() listingUpdated: Subject<OneListing> = new Subject();

public manualSave$$: Subject<void> = new Subject();
public isEqual$$: Subject<boolean> = new Subject();

public form = createForm<OneListing, OneListingForm>(this, {
formType: FormType.ROOT,
disabled$: this.disabled$,
input$: this.input$,
output$: this.listingUpdated,
manualSave$: this.manualSave$$,
isEqual$: this.isEqual$$,
formControls: {
vehicleProduct: new UntypedFormControl(null),
droidProduct: new UntypedFormControl(null),
Expand Down