Skip to content

Commit

Permalink
feat(reactive-forms): add invalid$ and valid$
Browse files Browse the repository at this point in the history
Add invalid$ and valid$ control status properties to controls.
  • Loading branch information
Jonathan Faircloth committed Apr 10, 2022
1 parent 974f46b commit f232fa1
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 52 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,28 @@ const control = new FormControl('');
control.enabled$.subscribe(isEnabled => ...);
```

### `invalid$`

Observes the control's `invalid` status.

```ts
import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.invalid$.subscribe(isInvalid => ...);
```

### `valid$`

Observes the control's `valid` status.

```ts
import { FormControl } from '@ngneat/reactive-forms';

const control = new FormControl('');
control.valid$.subscribe(isInvalid => ...);
```

### `status$`

Observes the control's `status`.
Expand Down
4 changes: 3 additions & 1 deletion libs/reactive-forms/src/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export function controlValueChanges$<T>(

export type ControlState = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';

export function controlStatus$<K extends 'disabled' | 'enabled' | 'status'>(
export function controlStatus$<
K extends 'disabled' | 'enabled' | 'invalid' | 'valid' | 'status'
>(
control: AbstractControl,
type: K
): Observable<K extends 'status' ? ControlState : boolean> {
Expand Down
25 changes: 25 additions & 0 deletions libs/reactive-forms/src/lib/form-array.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Validators } from '@angular/forms';
import { expectTypeOf } from 'expect-type';
import { Observable, of, Subject, Subscription } from 'rxjs';
import { FormControl, FormGroup } from '..';
Expand Down Expand Up @@ -33,6 +34,8 @@ describe('FormArray Types', () => {

expectTypeOf(arr.disabled$).toEqualTypeOf<Observable<boolean>>();
expectTypeOf(arr.enabled$).toEqualTypeOf<Observable<boolean>>();
expectTypeOf(arr.invalid$).toEqualTypeOf<Observable<boolean>>();
expectTypeOf(arr.valid$).toEqualTypeOf<Observable<boolean>>();
expectTypeOf(arr.status$).toEqualTypeOf<Observable<ControlState>>();

const first$ = arr.select((state) => {
Expand Down Expand Up @@ -207,6 +210,28 @@ describe('FormArray Functionality', () => {
expect(spy).toHaveBeenCalledTimes(2);
});

it('should invalidChanges$', () => {
const control = new FormArray([new FormControl(null, Validators.required)]);
const spy = jest.fn();
control.invalid$.subscribe(spy);
expect(spy).toHaveBeenCalledWith(true);
control.setValue(['abc']);
expect(spy).toHaveBeenCalledWith(false);
control.setValue([null]);
expect(spy).toHaveBeenCalledTimes(3);
});

it('should validChanges$', () => {
const control = new FormArray([new FormControl(null, Validators.required)]);
const spy = jest.fn();
control.valid$.subscribe(spy);
expect(spy).toHaveBeenCalledWith(false);
control.setValue(['abc']);
expect(spy).toHaveBeenCalledWith(true);
control.setValue([null]);
expect(spy).toHaveBeenCalledTimes(3);
});

it('should statusChanges$', () => {
const control = createArray();
const spy = jest.fn();
Expand Down
8 changes: 5 additions & 3 deletions libs/reactive-forms/src/lib/form-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import { DeepPartial } from './types';
export class FormArray<
T,
Control extends AbstractControl = T extends Record<any, any>
? FormGroup<ControlsOf<T>>
: FormControl<T>
> extends NgFormArray {
? FormGroup<ControlsOf<T>>
: FormControl<T>
> extends NgFormArray {
readonly value!: T[];
readonly valueChanges!: Observable<T[]>;

Expand All @@ -43,6 +43,8 @@ export class FormArray<
readonly value$ = controlValueChanges$<T[]>(this);
readonly disabled$ = controlStatus$(this, 'disabled');
readonly enabled$ = controlStatus$(this, 'enabled');
readonly invalid$ = controlStatus$(this, 'invalid');
readonly valid$ = controlStatus$(this, 'valid');
readonly status$ = controlStatus$(this, 'status');
readonly errors$ = controlErrorChanges$(
this,
Expand Down
22 changes: 22 additions & 0 deletions libs/reactive-forms/src/lib/form-control.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ describe('FormControl Functionality', () => {
expect(spy).toHaveBeenCalledTimes(2);
});

it('should invalidChanges$', () => {
const control = new FormControl<string | null>(null, Validators.required);
const spy = jest.fn();
control.invalid$.subscribe(spy);
expect(spy).toHaveBeenCalledWith(true);
control.setValue('abc');
expect(spy).toHaveBeenCalledWith(false);
control.setValue(null);
expect(spy).toHaveBeenCalledTimes(3);
});

it('should validChanges$', () => {
const control = new FormControl<string | null>(null, Validators.required);
const spy = jest.fn();
control.valid$.subscribe(spy);
expect(spy).toHaveBeenCalledWith(false);
control.setValue('abc');
expect(spy).toHaveBeenCalledWith(true);
control.setValue(null);
expect(spy).toHaveBeenCalledTimes(3);
});

it('should statusChanges$', () => {
const control = new FormControl<string>();
const spy = jest.fn();
Expand Down
3 changes: 2 additions & 1 deletion libs/reactive-forms/src/lib/form-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
} from './core';
import { BoxedValue } from './types';


export class FormControl<T> extends NgFormControl {
readonly value!: T;
readonly valueChanges!: Observable<T>;
Expand All @@ -37,6 +36,8 @@ export class FormControl<T> extends NgFormControl {
readonly value$ = controlValueChanges$<T>(this);
readonly disabled$ = controlStatus$(this, 'disabled');
readonly enabled$ = controlStatus$(this, 'enabled');
readonly invalid$ = controlStatus$(this, 'invalid');
readonly valid$ = controlStatus$(this, 'valid');
readonly status$ = controlStatus$(this, 'status');
readonly errors$ = controlErrorChanges$(
this,
Expand Down
124 changes: 77 additions & 47 deletions libs/reactive-forms/src/lib/form-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expectTypeOf } from 'expect-type';
import { FormGroup } from './form-group';
import { FormControl } from './form-control';
import { FormArray } from './form-array';
import { AbstractControl } from '@angular/forms';
import { AbstractControl, Validators } from '@angular/forms';
import { Observable, of, Subject, Subscription } from 'rxjs';
import { ControlsOf } from '..';
import { ValuesOf } from './types';
Expand Down Expand Up @@ -68,6 +68,32 @@ describe('FormGroup Functionality', () => {
expect(spy).toHaveBeenCalledTimes(2);
});

it('should invalidChanges$', () => {
const control = new FormGroup({
name: new FormControl<string | null>(null, Validators.required),
});
const spy = jest.fn();
control.invalid$.subscribe(spy);
expect(spy).toHaveBeenCalledWith(true);
control.setValue({ name: 'abc' });
expect(spy).toHaveBeenCalledWith(false);
control.setValue({ name: null });
expect(spy).toHaveBeenCalledTimes(3);
});

it('should validChanges$', () => {
const control = new FormGroup({
name: new FormControl<string | null>(null, Validators.required),
});
const spy = jest.fn();
control.valid$.subscribe(spy);
expect(spy).toHaveBeenCalledWith(false);
control.setValue({ name: 'abc' });
expect(spy).toHaveBeenCalledWith(true);
control.setValue({ name: null });
expect(spy).toHaveBeenCalledTimes(3);
});

it('should statusChanges$', () => {
const control = createGroup();
const spy = jest.fn();
Expand Down Expand Up @@ -198,7 +224,9 @@ describe('FormGroup Functionality', () => {

function areAllAllChildrenDirty(control: AbstractControl) {
expect(control.dirty).toBe(true);
(control as any)._forEachChild((control: AbstractControl) => areAllAllChildrenDirty(control));
(control as any)._forEachChild((control: AbstractControl) =>
areAllAllChildrenDirty(control)
);
}

it('should markAllAsDirty', () => {
Expand Down Expand Up @@ -331,6 +359,8 @@ describe('FormGroup Types', () => {

expectTypeOf(group.disabled$).toEqualTypeOf<Observable<boolean>>();
expectTypeOf(group.enabled$).toEqualTypeOf<Observable<boolean>>();
expectTypeOf(group.invalid$).toEqualTypeOf<Observable<boolean>>();
expectTypeOf(group.valid$).toEqualTypeOf<Observable<boolean>>();
expectTypeOf(group.status$).toEqualTypeOf<Observable<ControlState>>();

const name$ = group.select((state) => {
Expand Down Expand Up @@ -467,22 +497,19 @@ describe('FormGroup Types', () => {
});
});



describe('ControlsOf', () => {

it('should infer the type', () => {
interface Foo {
str: string;
nested: {
one: string;
two: number,
two: number;
deep: {
id: number;
arr: string[]
}
},
arr: string[]
arr: string[];
};
};
arr: string[];
}

const group = new FormGroup<ControlsOf<Foo>>({
Expand All @@ -492,56 +519,57 @@ describe('ControlsOf', () => {
two: new FormControl(),
deep: new FormGroup({
id: new FormControl(1),
arr: new FormArray([])
})
arr: new FormArray([]),
}),
}),
arr: new FormArray([])
arr: new FormArray([]),
});

expectTypeOf(group.value).toEqualTypeOf<Foo>();

expectTypeOf(group.get('str')).toEqualTypeOf<FormControl<string>>();
expectTypeOf(group.get('nested')).toEqualTypeOf<FormGroup<ControlsOf<Foo['nested']>>>();
expectTypeOf(group.get('arr')).toEqualTypeOf<FormArray<string, FormControl<string>>>();
expectTypeOf(group.get('nested')).toEqualTypeOf<
FormGroup<ControlsOf<Foo['nested']>>
>();
expectTypeOf(group.get('arr')).toEqualTypeOf<
FormArray<string, FormControl<string>>
>();

expectTypeOf(group.get('nested').value).toEqualTypeOf<Foo['nested']>();
expectTypeOf(group.get('arr').value).toEqualTypeOf<Foo['arr']>();


new FormGroup<ControlsOf<Foo>>({
// @ts-expect-error - should be typed
str: new FormControl(1),
// @ts-expect-error - should be typed
nested: new FormGroup({
// one: new FormControl(''),
two: new FormControl()
two: new FormControl(),
}),
// @ts-expect-error - should be typed
arr: new FormArray([new FormControl(1)])
})
})
arr: new FormArray([new FormControl(1)]),
});
});

it('should allow FormControls as objects or arrays', () => {

interface Bar {
str: string;
controlGroup: FormControl<{
one: string;
two: number
}>,
controlArr: FormControl<string[]>,
two: number;
}>;
controlArr: FormControl<string[]>;
group: {
id: string;
deep: {
id: number;
arr: FormControl<string[]>
}
}
arr: string[],
arrGroup: Array<{ name: string, count: number }>;
arr: FormControl<string[]>;
};
};
arr: string[];
arrGroup: Array<{ name: string; count: number }>;
}


const group = new FormGroup<ControlsOf<Bar>>({
str: new FormControl(''),
controlGroup: new FormControl({ one: '', two: 1 }),
Expand All @@ -550,31 +578,32 @@ describe('ControlsOf', () => {
id: new FormControl(),
deep: new FormGroup({
id: new FormControl(),
arr: new FormControl([])
})
arr: new FormControl([]),
}),
}),
arr: new FormArray([]),
arrGroup: new FormArray([])
arrGroup: new FormArray([]),
});

expectTypeOf(group.value).toEqualTypeOf<ValuesOf<ControlsOf<Bar>>>();

new FormGroup<ControlsOf<Bar>>({
str: new FormControl(''),
// @ts-expect-error - should be FormControl
controlGroup: new FormGroup({ one: new FormControl(''), two: new FormControl() }),
controlGroup: new FormGroup({
one: new FormControl(''),
two: new FormControl(),
}),
// @ts-expect-error - should be FormControl
controlArr: new FormArray([]),
// @ts-expect-error - should be FormGroup
group: new FormControl(),
// @ts-expect-error - should be FormArray
arr: new FormControl([]),
// @ts-expect-error - should be FormArray
arrGroup: new FormControl([])
arrGroup: new FormControl([]),
});

})

});

it('should work with optional fields', () => {
type Foo = {
Expand All @@ -583,29 +612,30 @@ describe('ControlsOf', () => {
baz: null | string;
arr?: string[];
nested: {
id: string
}
}
id: string;
};
};

const group = new FormGroup<ControlsOf<Foo>>({
foo: new FormControl(''),
name: new FormControl(''),
baz: new FormControl(null),
arr: new FormArray([]),
nested: new FormGroup({
id: new FormControl('')
})
})
id: new FormControl(''),
}),
});

// @ts-expect-error - should be a string
group.get('name')?.patchValue(1);

expectTypeOf(group.get('name')).toEqualTypeOf<FormControl<string | undefined> | undefined>();
expectTypeOf(group.get('name')).toEqualTypeOf<
FormControl<string | undefined> | undefined
>();

expectTypeOf(group.value.name).toEqualTypeOf<string | undefined>();
expectTypeOf(group.value.arr).toEqualTypeOf<string[] | undefined>();
expectTypeOf(group.value.baz).toEqualTypeOf<string | null>();
expectTypeOf(group.value.nested).toEqualTypeOf<{ id: string }>();
})

});
});
Loading

0 comments on commit f232fa1

Please sign in to comment.