Skip to content

Commit

Permalink
Merge pull request #38 from cloudnc/fix/emit-form-changes
Browse files Browse the repository at this point in the history
Fix/emit form changes
  • Loading branch information
zak-cloudnc authored May 21, 2019
2 parents 7015a1f + be938ff commit d609e60
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 62 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ interface OneListingForm {
title: string;
price: number;

// polymorphic form where product can either be a vehicule or a droid
// polymorphic form where product can either be a vehicle or a droid
listingType: ListingType | null;
vehicleProduct: OneVehicle | null;
droidProduct: OneDroid | null;
Expand Down Expand Up @@ -294,7 +294,7 @@ export class VehicleProductComponent extends NgxSubFormRemapComponent<OneVehicle
}
```

Our "incoming" object is of type `OneVehicle` but into that component we treat it as a `OneVehicleForm` to split the vehicule (either a `speeder` or `spaceship`) in 2 **separated** properties.
Our "incoming" object is of type `OneVehicle` but into that component we treat it as a `OneVehicleForm` to split the vehicle (either a `speeder` or `spaceship`) in 2 **separated** properties.

### Helpers

Expand Down
6 changes: 3 additions & 3 deletions cypress/helpers/data.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export interface ListElement {
readonly details: string;
}

export type VehiculeFormElement = {
readonly vehiculeType: string;
export type VehicleFormElement = {
readonly vehicleType: string;
} & (
| {
readonly spaceshipForm: {
Expand Down Expand Up @@ -50,7 +50,7 @@ export interface FormElement {
readonly imageUrl: string;
readonly price: string;
readonly listingType: string;
} & ({ readonly vehiculeForm: VehiculeFormElement });
} & ({ readonly vehicleForm: VehicleFormElement });
}

export const hardcodedElementToTestElement = (item: OneListing): ListElement => {
Expand Down
11 changes: 4 additions & 7 deletions cypress/helpers/dom.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const DOM = {
return cy.get(`*[data-no-error]`);
},
get obj(): Cypress.Chainable<FormElement> {
const getVehiculeObj = (element: HTMLElement, type: VehicleType) =>
const getVehicleObj = (element: HTMLElement, type: VehicleType) =>
({
Spaceship: {
spaceshipForm: {
Expand Down Expand Up @@ -120,12 +120,9 @@ export const DOM = {
imageUrl: getTextFromInput(element, 'input-image-url'),
price: getTextFromInput(element, 'input-price'),
listingType: getSelectedOptionFromSelect(element, 'select-listing-type'),
vehiculeForm: {
vehiculeType: getSelectedOptionFromSelect(element, 'select-vehicule-type'),
...getVehiculeObj(element, getSelectedOptionFromSelect(
element,
'select-vehicule-type',
) as VehicleType),
vehicleForm: {
vehicleType: getSelectedOptionFromSelect(element, 'select-vehicle-type'),
...getVehicleObj(element, getSelectedOptionFromSelect(element, 'select-vehicle-type') as VehicleType),
},
},
}))
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"lib:test:ci": "yarn run ng test --project ngx-sub-form --watch false",
"------------------ Quick Commands ------------------": "",
"lint:fix": "yarn demo:lint:fix && yarn prettier:write",
"semantic-release": "semantic-release"
"semantic-release": "semantic-release",
"test": "yarn lib:test:watch"
},
"private": true,
"resolutions": {
Expand Down
12 changes: 0 additions & 12 deletions projects/ngx-sub-form/src/helpers/utils.ts

This file was deleted.

75 changes: 51 additions & 24 deletions projects/ngx-sub-form/src/lib/ngx-sub-form.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,38 +306,59 @@ describe(`NgxSubFormComponent`, () => {
done();
}, 0);
});

it(`should correctly emit the onChange value only once when form is patched locally`, (done: () => void) => {
const spyOnFormUpdate = jasmine.createSpy();
const spyOnChange = jasmine.createSpy();
subComponent.onFormUpdate = spyOnFormUpdate;
subComponent.registerOnChange(spyOnChange);
(subComponent as any).emitInitialValueOnInit = false;

subComponent.formGroup.patchValue({ color: 'red', canFire: false });

setTimeout(() => {
expect(spyOnFormUpdate).toHaveBeenCalledWith({
canFire: true,
});

expect(spyOnChange).toHaveBeenCalledTimes(1);
expect(spyOnChange).toHaveBeenCalledWith({ color: 'red', canFire: false, numberOfPeopleOnBoard: 10 });

done();
}, 0);
});
});
});

interface VehiculeForm {
vehiculeColor: Vehicle['color'] | null;
vehiculeCanFire: Vehicle['canFire'] | null;
vehiculeNumberOfPeopleOnBoard: Vehicle['numberOfPeopleOnBoard'] | null;
interface VehicleForm {
vehicleColor: Vehicle['color'] | null;
vehicleCanFire: Vehicle['canFire'] | null;
vehicleNumberOfPeopleOnBoard: Vehicle['numberOfPeopleOnBoard'] | null;
}

class SubRemapComponent extends NgxSubFormRemapComponent<Vehicle, VehiculeForm> {
class SubRemapComponent extends NgxSubFormRemapComponent<Vehicle, VehicleForm> {
getFormControls() {
// even though optional, if we comment out vehiculeColor there should be a TS error
// even though optional, if we comment out vehicleColor there should be a TS error
return {
vehiculeColor: new FormControl(getDefaultValues().color),
vehiculeCanFire: new FormControl(getDefaultValues().canFire),
vehiculeNumberOfPeopleOnBoard: new FormControl(getDefaultValues().numberOfPeopleOnBoard),
vehicleColor: new FormControl(getDefaultValues().color),
vehicleCanFire: new FormControl(getDefaultValues().canFire),
vehicleNumberOfPeopleOnBoard: new FormControl(getDefaultValues().numberOfPeopleOnBoard),
};
}

protected transformToFormGroup(obj: Vehicle | null): VehiculeForm {
protected transformToFormGroup(obj: Vehicle | null): VehicleForm {
return {
vehiculeColor: obj ? obj.color : null,
vehiculeCanFire: obj ? obj.canFire : null,
vehiculeNumberOfPeopleOnBoard: obj ? obj.numberOfPeopleOnBoard : null,
vehicleColor: obj ? obj.color : null,
vehicleCanFire: obj ? obj.canFire : null,
vehicleNumberOfPeopleOnBoard: obj ? obj.numberOfPeopleOnBoard : null,
};
}

protected transformFromFormGroup(formValue: VehiculeForm): Vehicle | null {
protected transformFromFormGroup(formValue: VehicleForm): Vehicle | null {
return {
color: formValue.vehiculeColor,
canFire: formValue.vehiculeCanFire,
numberOfPeopleOnBoard: formValue.vehiculeNumberOfPeopleOnBoard,
color: formValue.vehicleColor,
canFire: formValue.vehicleCanFire,
numberOfPeopleOnBoard: formValue.vehicleNumberOfPeopleOnBoard,
};
}
}
Expand All @@ -363,9 +384,9 @@ describe(`NgxSubFormRemapComponent`, () => {

expect(subRemapComponent.formGroup.setValue).toHaveBeenCalledWith(
{
vehiculeColor: getDefaultValues().color,
vehiculeCanFire: getDefaultValues().canFire,
vehiculeNumberOfPeopleOnBoard: getDefaultValues().numberOfPeopleOnBoard,
vehicleColor: getDefaultValues().color,
vehicleCanFire: getDefaultValues().canFire,
vehicleNumberOfPeopleOnBoard: getDefaultValues().numberOfPeopleOnBoard,
},
{
emitEvent: false,
Expand Down Expand Up @@ -395,14 +416,20 @@ describe(`NgxSubFormRemapComponent`, () => {

onChangeSpy.calls.reset();

const newExpectation = {
color: 'green',
canFire: false,
numberOfPeopleOnBoard: 12,
};

subRemapComponent.formGroup.setValue({
vehiculeColor: getDefaultValues().color,
vehiculeCanFire: getDefaultValues().canFire,
vehiculeNumberOfPeopleOnBoard: getDefaultValues().numberOfPeopleOnBoard,
vehicleColor: newExpectation.color,
vehicleCanFire: newExpectation.canFire,
vehicleNumberOfPeopleOnBoard: newExpectation.numberOfPeopleOnBoard,
});

setTimeout(() => {
expect(onChangeSpy).toHaveBeenCalledWith(expectedValue);
expect(onChangeSpy).toHaveBeenCalledWith(newExpectation);

done();
}, 0);
Expand Down
15 changes: 7 additions & 8 deletions projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { OnDestroy } from '@angular/core';
import { ControlValueAccessor, FormGroup, ValidationErrors, Validator, AbstractControl } from '@angular/forms';
import { Subscription, combineLatest, merge, Observable } from 'rxjs';
import { delay, filter, startWith, map, withLatestFrom } from 'rxjs/operators';
import { AbstractControl, ControlValueAccessor, FormGroup, ValidationErrors, Validator } from '@angular/forms';
import { merge, Observable, Subscription } from 'rxjs';
import { delay, filter, map, startWith, withLatestFrom } from 'rxjs/operators';
import { ControlMap, Controls, ControlsNames, FormUpdate } from './ngx-sub-form-utils';
import { keyValuePairToObj } from '../helpers/utils';

interface OnFormUpdate<FormInterface> {
onFormUpdate?: (formUpdate: FormUpdate<FormInterface>) => void;
Expand Down Expand Up @@ -180,18 +179,18 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
),
);

const keyLastEmit$: Observable<keyof FormInterface> = merge(...formValues.map(obs => obs.pipe(map(x => x.key))));
const lastKeyEmitted$: Observable<keyof FormInterface> = merge(...formValues.map(obs => obs.pipe(map(x => x.key))));

this.subscription = combineLatest<KeyValueForm[]>(...formValues)
this.subscription = this.formGroup.valueChanges
.pipe(
startWith(this.formGroup.value),
filter(() => !!this.formGroup),
// this is required otherwise an `ExpressionChangedAfterItHasBeenCheckedError` will happen
// this is due to the fact that parent component will define a given state for the form that might
// be changed once the children are being initialized
delay(0),
map(x => keyValuePairToObj<FormInterface>(x)),
// detect which stream emitted last
withLatestFrom(keyLastEmit$),
withLatestFrom(lastKeyEmitted$),
map(([changes, keyLastEmit], index) => {
if (index > 0 && this.onTouched) {
this.onTouched();
Expand Down
8 changes: 4 additions & 4 deletions src/app/app.spec.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ context(`EJawa demo`, () => {
imageUrl: x.imageUrl,
price: x.price + '',
listingType: x.listingType,
vehiculeForm: {
vehiculeType: x.product.vehicleType,
vehicleForm: {
vehicleType: x.product.vehicleType,
spaceshipForm: {
color: v.color,
canFire: v.canFire,
Expand Down Expand Up @@ -62,8 +62,8 @@ context(`EJawa demo`, () => {
imageUrl: x.imageUrl,
price: x.price + '',
listingType: x.listingType,
vehiculeForm: {
vehiculeType: x.product.vehicleType,
vehicleForm: {
vehicleType: x.product.vehicleType,
speederForm: {
color: v.color,
canFire: v.canFire,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<mat-form-field>
<mat-select
data-select-vehicule-type
data-select-vehicle-type
placeholder="Select vehicle type"
[formControlName]="formControlNames.vehicleType"
>
Expand Down

0 comments on commit d609e60

Please sign in to comment.