From 3624fad41a8f7ba0f59683ac695a66fd9b2d041e Mon Sep 17 00:00:00 2001 From: Joe Gilreath Date: Wed, 30 Aug 2023 15:10:59 -0400 Subject: [PATCH 1/3] feat: only emit values is status valid --- libs/reactive-forms/src/lib/core.ts | 11 +++- .../reactive-forms/src/lib/form-array.spec.ts | 28 +++++++++- libs/reactive-forms/src/lib/form-array.ts | 2 + .../src/lib/form-control.spec.ts | 13 +++++ libs/reactive-forms/src/lib/form-control.ts | 2 + .../reactive-forms/src/lib/form-group.spec.ts | 55 +++++++++++++++++++ libs/reactive-forms/src/lib/form-group.ts | 2 + 7 files changed, 111 insertions(+), 2 deletions(-) diff --git a/libs/reactive-forms/src/lib/core.ts b/libs/reactive-forms/src/lib/core.ts index 3336f2d..6e3d60d 100644 --- a/libs/reactive-forms/src/lib/core.ts +++ b/libs/reactive-forms/src/lib/core.ts @@ -1,6 +1,6 @@ import { AbstractControl, ValidationErrors } from '@angular/forms'; import { defer, merge, Observable, of, Subscription } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map } from 'rxjs/operators'; export function selectControlValue$( control: any, @@ -21,6 +21,15 @@ export function controlValueChanges$( ) as Observable; } +export function controlValidValueChanges$( + control: AbstractControl & { getRawValue: () => T } +): Observable { + return merge( + defer(() => of(control.getRawValue())), + control.valueChanges.pipe(filter(() => control.valid), map(() => control.getRawValue())) + ) as Observable; +} + export type ControlState = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'; export function controlStatus$< diff --git a/libs/reactive-forms/src/lib/form-array.spec.ts b/libs/reactive-forms/src/lib/form-array.spec.ts index d6042e6..91f1820 100644 --- a/libs/reactive-forms/src/lib/form-array.spec.ts +++ b/libs/reactive-forms/src/lib/form-array.spec.ts @@ -1,7 +1,7 @@ import { Validators } from '@angular/forms'; import { expectTypeOf } from 'expect-type'; import { Observable, of, Subject, Subscription } from 'rxjs'; -import {ControlsOf, FormControl, FormGroup, ValuesOf} from '..'; +import {ControlsOf, FormControl, FormGroup} from '..'; import { ControlState } from './core'; import { FormArray } from './form-array'; @@ -224,6 +224,13 @@ const createArray = (withError = false) => { ); }; +const createInvalidArray = (withError = false) => { + return new FormArray( + [new FormControl(null, Validators.required), new FormControl(null, Validators.required)], + withError ? errorFn : [] + ); +}; + describe('FormArray Functionality', () => { it('should valueChanges$', () => { const control = createArray(); @@ -240,6 +247,25 @@ describe('FormArray Functionality', () => { expect(spy).toHaveBeenCalledWith(['1', '3', '']); }); + it('should validValueChanges$', () => { + const control = createInvalidArray(); + const spy = jest.fn(); + control.validValue$.subscribe(spy); + expect(spy).toHaveBeenCalledTimes(1); + control.patchValue(['1', '2']); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith(['1', '2']); + control.push(new FormControl('3', Validators.required)); + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith(['1', '2', '3']); + control.push(new FormControl(null, Validators.required)); + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).not.toHaveReturnedWith(['1', '2', '3', null]); + control.removeAt(3); + expect(spy).toHaveBeenCalledTimes(4); + expect(spy).toHaveBeenCalledWith(['1', '2', '3']); + }); + it('should disabledChanges$', () => { const control = createArray(); const spy = jest.fn(); diff --git a/libs/reactive-forms/src/lib/form-array.ts b/libs/reactive-forms/src/lib/form-array.ts index 46571b7..2fec97c 100644 --- a/libs/reactive-forms/src/lib/form-array.ts +++ b/libs/reactive-forms/src/lib/form-array.ts @@ -18,6 +18,7 @@ import { disableControl, enableControl, markAllDirty, + controlValidValueChanges$, } from './core'; import { DeepPartial } from './types'; @@ -46,6 +47,7 @@ export class FormArray< .asObservable() .pipe(distinctUntilChanged()); readonly value$ = controlValueChanges$>>(this); + readonly validValue$ = controlValidValueChanges$>>(this); readonly disabled$ = controlStatus$(this, 'disabled'); readonly enabled$ = controlStatus$(this, 'enabled'); readonly invalid$ = controlStatus$(this, 'invalid'); diff --git a/libs/reactive-forms/src/lib/form-control.spec.ts b/libs/reactive-forms/src/lib/form-control.spec.ts index f1fd3ce..6eba2b6 100644 --- a/libs/reactive-forms/src/lib/form-control.spec.ts +++ b/libs/reactive-forms/src/lib/form-control.spec.ts @@ -14,6 +14,19 @@ describe('FormControl Functionality', () => { expect(spy).toHaveBeenCalledWith('patched'); }); + it('should validValueChanges$', () => { + const control = new FormControl(null, Validators.required); + const spy = jest.fn(); + control.validValue$.subscribe(spy); + expect(spy).toHaveBeenCalledTimes(1); + control.patchValue('patched'); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith('patched'); + control.patchValue(''); + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).not.toHaveBeenCalledWith(''); + }); + it('should disabledChanges$', () => { const control = new FormControl(); const spy = jest.fn(); diff --git a/libs/reactive-forms/src/lib/form-control.ts b/libs/reactive-forms/src/lib/form-control.ts index ba1c320..e392dd0 100644 --- a/libs/reactive-forms/src/lib/form-control.ts +++ b/libs/reactive-forms/src/lib/form-control.ts @@ -16,6 +16,7 @@ import { removeError, hasErrorAnd, controlErrorChanges$, + controlValidValueChanges$, } from './core'; import { BoxedValue } from './types'; @@ -34,6 +35,7 @@ export class FormControl extends UntypedFormControl { .asObservable() .pipe(distinctUntilChanged()); readonly value$ = controlValueChanges$(this); + readonly validValue$ = controlValidValueChanges$(this); readonly disabled$ = controlStatus$(this, 'disabled'); readonly enabled$ = controlStatus$(this, 'enabled'); readonly invalid$ = controlStatus$(this, 'invalid'); diff --git a/libs/reactive-forms/src/lib/form-group.spec.ts b/libs/reactive-forms/src/lib/form-group.spec.ts index e701d32..b212177 100644 --- a/libs/reactive-forms/src/lib/form-group.spec.ts +++ b/libs/reactive-forms/src/lib/form-group.spec.ts @@ -25,6 +25,18 @@ const createGroup = () => { ); }; +const createInvalidGroup = () => { + return new FormGroup( + { + name: new FormControl(null, Validators.required), + phone: new FormGroup({ + num: new FormControl(null, Validators.required), + prefix: new FormControl(null, Validators.required), + }), + } + ); +}; + describe('FormGroup Functionality', () => { it('should valueChanges$', () => { const control = createGroup(); @@ -46,6 +58,49 @@ describe('FormGroup Functionality', () => { }); }); + it('should validValueChanges$', () => { + const control = createInvalidGroup(); + const spy = jest.fn(); + control.validValue$.subscribe(spy); + + expect(spy).toHaveBeenCalledTimes(1); + control.patchValue({ + name: 'jim', + phone: { + num: 0, + prefix: 1 + } + }); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledWith({ + name: 'jim', + phone: { num: 0, prefix: 1 }, + }); + + control.patchValue({ + name: 'changed', + }); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenCalledWith({ + name: 'changed', + phone: { num: 0, prefix: 1 }, + }); + + control.patchValue({ + phone: { + num: null + } + }); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).not.toHaveBeenCalledWith({ + name: 'changed', + phone: { num: null, prefix: 1 }, + }); + }); + it('should disabledChanges$', () => { const control = createGroup(); const spy = jest.fn(); diff --git a/libs/reactive-forms/src/lib/form-group.ts b/libs/reactive-forms/src/lib/form-group.ts index b403a45..53b780b 100644 --- a/libs/reactive-forms/src/lib/form-group.ts +++ b/libs/reactive-forms/src/lib/form-group.ts @@ -10,6 +10,7 @@ import { controlEnabledWhile, controlErrorChanges$, controlStatus$, + controlValidValueChanges$, controlValueChanges$, disableControl, enableControl, @@ -36,6 +37,7 @@ export class FormGroup> extends UntypedFormGroup { .asObservable() .pipe(distinctUntilChanged()); readonly value$ = controlValueChanges$>(this); + readonly validValue$ = controlValidValueChanges$>(this); readonly disabled$ = controlStatus$(this, 'disabled'); readonly enabled$ = controlStatus$(this, 'enabled'); readonly invalid$ = controlStatus$(this, 'invalid'); From 974e9dc4da493df9779e8c0825b3f553f659f5d8 Mon Sep 17 00:00:00 2001 From: Joe Gilreath Date: Wed, 30 Aug 2023 15:33:56 -0400 Subject: [PATCH 2/3] docs(readme): updated for valid value feature --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index aa33389..6e7d18c 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,17 @@ const control = new FormControl(''); control.value$.subscribe(value => ...); ``` +### `validValue$` + +Similar to value$; Observes the control's value and **only** emits if the control is valid. + +```ts +import { FormControl } from '@ngneat/reactive-forms'; + +const control = new FormControl(null, [Validators.required]); +control.validValue$.subscribe(value => ...); +``` + ### `disabled$` Observes the control's `disable` status. From cc5fb9a131aa7a681e432c09e465aa1f5a859d21 Mon Sep 17 00:00:00 2001 From: Joe Gilreath Date: Mon, 18 Sep 2023 14:24:36 -0400 Subject: [PATCH 3/3] fix: manual workflow trigger --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b565ca5..64a4a18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,14 @@ -name: '@ngneat/reactive-forms' +name: 'lothern/reactive-forms' on: + workflow_dispatch: push: branches: - master pull_request: - jobs: build: runs-on: ubuntu-latest - strategy: - fail-fast: true - steps: - uses: actions/checkout@v2 - name: Cache node modules