Skip to content

Commit

Permalink
feat(Array Handling): Use interface to manage array handling methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
zak-cloudnc authored and maxime1992 committed Jun 23, 2019
1 parent 18cdc74 commit 92fa38e
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 35 deletions.
2 changes: 2 additions & 0 deletions projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export type KeysWithType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[

export type ArrayPropertyOf<T> = KeysWithType<T, Array<any>>;

export type ArrayTypeOfPropertyOf<T, K extends keyof T = keyof T> = T[K] extends Array<infer U> ? U : never;

export function subformComponentProviders(
component: any,
): {
Expand Down
34 changes: 19 additions & 15 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 @@ -8,7 +8,7 @@ import {
MissingFormControlsError,
NGX_SUB_FORM_HANDLE_VALUE_CHANGES_RATE_STRATEGIES,
Controls,
ArrayPropertyOf,
ArrayPropertyOf, ArrayTypeOfPropertyOf, NgxFormWithArrayControls,
} from '../public_api';
import { Observable } from 'rxjs';

Expand Down Expand Up @@ -74,9 +74,7 @@ describe(`NgxSubFormComponent`, () => {

// we have to call `updateValueAndValidity` within the constructor in an async way
// and here we need to wait for it to run
setTimeout(() => {
done();
}, 0);
setTimeout(done);
});

describe(`created`, () => {
Expand Down Expand Up @@ -588,7 +586,7 @@ interface VehiclesArrayForm {
vehicles: Vehicle[];
}

class SubArrayComponent extends NgxSubFormRemapComponent<Vehicle[], VehiclesArrayForm> {
class SubArrayComponent extends NgxSubFormRemapComponent<Vehicle[], VehiclesArrayForm> implements NgxFormWithArrayControls<VehiclesArrayForm> {
protected getFormControls(): Controls<VehiclesArrayForm> {
return {
vehicles: new FormArray([]),
Expand All @@ -605,22 +603,20 @@ class SubArrayComponent extends NgxSubFormRemapComponent<Vehicle[], VehiclesArra
return formValue.vehicles;
}

public createFormArrayControl(key: ArrayPropertyOf<VehiclesArrayForm> | undefined): FormControl {
return new FormControl(null, [Validators.required]);
public createFormArrayControl(key: ArrayPropertyOf<VehiclesArrayForm> | undefined, initialValue: ArrayTypeOfPropertyOf<VehiclesArrayForm>): FormControl {
return new FormControl(initialValue, [Validators.required]);
}
}

describe(`NgxSubFormArrayComponent`, () => {
describe(`SubArrayComponent`, () => {
let subArrayComponent: SubArrayComponent;

beforeEach((done: () => void) => {
subArrayComponent = new SubArrayComponent();

// we have to call `updateValueAndValidity` within the constructor in an async way
// and here we need to wait for it to run
setTimeout(() => {
done();
}, 0);
setTimeout(done);
});

it(`should have the correct values within the 'FormArray'`, () => {
Expand Down Expand Up @@ -663,14 +659,17 @@ describe(`NgxSubFormArrayComponent`, () => {

subArrayComponent.writeValue([...values, newValue]);

// check the form controls are the exact same instances
expect(subArrayComponent.formGroupControls.vehicles.at(0)).toBe(fc1);
expect(subArrayComponent.formGroupControls.vehicles.at(1)).toBe(fc2);

// check the values are unchanged
expect(subArrayComponent.formGroupControls.vehicles.at(0).value).toBe(values[0]);
expect(subArrayComponent.formGroupControls.vehicles.at(1).value).toBe(values[1]);
expect(subArrayComponent.formGroupControls.vehicles.at(2).value).toBe(newValue);
});

it(`should be possible to create a FormControl from the 'createFormArrayControl' hook based on the current property`, () => {
const onChangeSpy = jasmine.createSpy('onChangeSpy');

subArrayComponent.registerOnChange(onChangeSpy);

const createFormArrayControl = spyOn(subArrayComponent, 'createFormArrayControl').and.callThrough();

Expand All @@ -682,6 +681,11 @@ describe(`NgxSubFormArrayComponent`, () => {
subArrayComponent.writeValue(values);

expect(createFormArrayControl).toHaveBeenCalledTimes(2);
expect(createFormArrayControl).toHaveBeenCalledWith('vehicles');
// check values
expect(createFormArrayControl).toHaveBeenCalledWith('vehicles', values[0]);
expect(createFormArrayControl).toHaveBeenCalledWith('vehicles', values[1]);
// check non-default control was created
expect(subArrayComponent.formGroupControls.vehicles.at(0).validator).not.toBe(null);
});

});
26 changes: 15 additions & 11 deletions projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import {
FormErrors,
isNullOrUndefined,
ControlsType,
ArrayPropertyOf,
ArrayPropertyOf, ArrayTypeOfPropertyOf,
} from './ngx-sub-form-utils';
import { FormGroupOptions, OnFormUpdate, TypedFormGroup } from './ngx-sub-form.types';
import { FormGroupOptions, NgxFormWithArrayControls, OnFormUpdate, TypedFormGroup } from './ngx-sub-form.types';

type MapControlFunction<FormInterface, MapValue> = (ctrl: AbstractControl, key: keyof FormInterface) => MapValue;
type FilterControlFunction<FormInterface> = (ctrl: AbstractControl, key: keyof FormInterface) => boolean;
Expand Down Expand Up @@ -228,13 +228,24 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
formArray.removeAt(formArray.length - 1);
}

while (formArray.length < value.length) {
formArray.push(this.createFormArrayControl(key as ArrayPropertyOf<FormInterface>));
for (let i = formArray.length; i < value.length; i++) {

if (this.formIsFormWithArrayControls()) {
formArray.insert(i, this.createFormArrayControl(key as ArrayPropertyOf<FormInterface>, value[i]));
} else {
formArray.insert(i, new FormControl(value[i]));
}

}

}
});
}

private formIsFormWithArrayControls(): this is NgxFormWithArrayControls<FormInterface> {
return typeof ((this as unknown) as NgxFormWithArrayControls<FormInterface>).createFormArrayControl === 'function';
}

private getMissingKeys(transformedValue: FormInterface | null) {
// `controlKeys` can be an empty array, empty forms are allowed
const missingKeys: (keyof FormInterface)[] = this.controlKeys.reduce(
Expand All @@ -251,13 +262,6 @@ export abstract class NgxSubFormComponent<ControlInterface, FormInterface = Cont
return missingKeys;
}

// override this hook to customize the creation of a FormControl within a FormArray
// the undefined is required here as otherwise classes that are not overriding that hook
// and do not have an array in the `FormInterface` would error as the type of key would be undefined
protected createFormArrayControl(key: ArrayPropertyOf<FormInterface> | undefined): FormControl {
return new FormControl();
}

// when customizing the emission rate of your sub form component, remember not to **mutate** the stream
// it is safe to throttle, debounce, delay, etc but using skip, first, last or mutating data inside
// the stream will cause issues!
Expand Down
10 changes: 8 additions & 2 deletions projects/ngx-sub-form/src/lib/ngx-sub-form.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormGroup, ValidationErrors } from '@angular/forms';
import { FormControl, FormGroup, ValidationErrors } from '@angular/forms';
import { Observable } from 'rxjs';
import { Controls, FormUpdate } from './ngx-sub-form-utils';
import { ArrayPropertyOf, ArrayTypeOfPropertyOf, Controls, FormUpdate } from './ngx-sub-form-utils';

export interface OnFormUpdate<FormInterface> {
onFormUpdate?: (formUpdate: FormUpdate<FormInterface>) => void;
Expand Down Expand Up @@ -40,3 +40,9 @@ export interface FormGroupOptions<T> {
*/
updateOn?: 'change' | 'blur' | 'submit';
}

// Unfortunately due to https://github.com/microsoft/TypeScript/issues/13995#issuecomment-504664533 the initial value
// cannot be fully type narrowed to the exact type that will be passed.
export interface NgxFormWithArrayControls<T> {
createFormArrayControl(key: ArrayPropertyOf<T>, initialValue: ArrayTypeOfPropertyOf<T>): FormControl;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Component } from '@angular/core';
import { FormArray, FormControl, Validators } from '@angular/forms';
import { Controls, NgxSubFormRemapComponent, subformComponentProviders, ArrayPropertyOf } from 'ngx-sub-form';
import {
Controls,
NgxSubFormRemapComponent,
subformComponentProviders,
ArrayPropertyOf,
ArrayTypeOfPropertyOf, NgxFormWithArrayControls,
} from 'ngx-sub-form';
import { CrewMember } from '../../../../../interfaces/crew-member.interface';

interface CrewMembersForm {
Expand All @@ -13,7 +19,7 @@ interface CrewMembersForm {
styleUrls: ['./crew-members.component.scss'],
providers: subformComponentProviders(CrewMembersComponent),
})
export class CrewMembersComponent extends NgxSubFormRemapComponent<CrewMember[], CrewMembersForm> {
export class CrewMembersComponent extends NgxSubFormRemapComponent<CrewMember[], CrewMembersForm> implements NgxFormWithArrayControls<CrewMembersForm> {
protected getFormControls(): Controls<CrewMembersForm> {
return {
crewMembers: new FormArray([]),
Expand All @@ -35,20 +41,23 @@ export class CrewMembersComponent extends NgxSubFormRemapComponent<CrewMember[],
}

public addCrewMember(): void {
this.formGroupControls.crewMembers.push(new FormControl());
this.formGroupControls.crewMembers.push(this.createFormArrayControl('crewMembers', {
firstName: '',
lastName: '',
}));
}

// following method is not required and return by default a simple FormControl
// if needed, you can use the `createFormArrayControl` hook to customize the creation
// of your `FormControl`s that will be added to the `FormArray`
protected createFormArrayControl(key: ArrayPropertyOf<CrewMembersForm>): FormControl {
public createFormArrayControl(key: ArrayPropertyOf<CrewMembersForm> | undefined, initialValue: ArrayTypeOfPropertyOf<CrewMembersForm>): FormControl {

switch (key) {
// note: the following string is type safe based on your form properties!
case 'crewMembers':
return new FormControl(null, [Validators.required]);

return new FormControl(initialValue, [Validators.required]);
default:
return new FormControl(null);
return new FormControl(initialValue);
}
}
}

0 comments on commit 92fa38e

Please sign in to comment.