From 62e8f654bd8fa5d4b02c814537d3369c5986e52c Mon Sep 17 00:00:00 2001 From: arturovt Date: Thu, 2 Mar 2023 10:57:08 +0200 Subject: [PATCH] feat(form-plugin): allow `ngxsFormDebounce` to be string --- CHANGELOG.md | 1 + docs/plugins/form.md | 78 +++++++++++++++---- packages/form-plugin/src/directive.ts | 19 +++-- .../form-plugin/tests/form.plugin.spec.ts | 6 +- ...e-1590-update-form-value-primitive.spec.ts | 2 + ...e-1822-multiple-ngxs-form-bindings.spec.ts | 3 +- .../form-plugin/tests/property-path.spec.ts | 3 +- 7 files changed, 86 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10485be11..207670fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Feature: Storage Plugin - Enable providing storage engine individually [#1935](https://github.com/ngxs/store/pull/1935) - Feature: Devtools Plugin - Add new options to the `NgxsDevtoolsOptions` interface [#1879](https://github.com/ngxs/store/pull/1879) - Feature: Devtools Plugin - Add trace options to `NgxsDevtoolsOptions` [#1968](https://github.com/ngxs/store/pull/1968) +- Feature: Form Plugin - Allow `ngxsFormDebounce` to be string [#1972](https://github.com/ngxs/store/pull/1972) - Performance: Tree-shake patch errors [#1955](https://github.com/ngxs/store/pull/1955) - Fix: Get descriptor explicitly when it's considered as a class property [#1961](https://github.com/ngxs/store/pull/1961) - Fix: Avoid delayed updates from state stream [#1981](https://github.com/ngxs/store/pull/1981) diff --git a/docs/plugins/form.md b/docs/plugins/form.md index 20c69e541..5596770e4 100644 --- a/docs/plugins/form.md +++ b/docs/plugins/form.md @@ -14,7 +14,7 @@ In a nutshell, this plugin helps to keep your forms and state in sync. ## Installation ```bash -npm install @ngxs/form-plugin --save +npm install @ngxs/form-plugin # or if you are using yarn yarn add @ngxs/form-plugin @@ -104,7 +104,9 @@ Now anytime your form updates, your state will also reflect the new state. The directive also has two inputs you can utilize as well: -- `ngxsFormDebounce: number` - Debounce the value changes to the form. Default value: `100`. Ignored if `updateOn` is `blur` or `submit`. +- `ngxsFormDebounce: number | string` - Debounce the value changes from the form. Default value: `100`. Ignored if: + - the provided value is less than `0` (for instance, `ngxsFormDebounce="-1"` is valid) + - `updateOn` is `blur` or `submit` - `ngxsFormClearOnDestroy: boolean` - Clear the state on destroy of the form. ### Actions @@ -164,7 +166,7 @@ The state contains information about the new novel name and its authors. Let's c ```ts import { Component } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder } from '@angular/forms'; @Component({ selector: 'new-novel-form', @@ -184,18 +186,16 @@ import { FormBuilder, FormGroup } from '@angular/forms'; ` }) export class NewNovelComponent { - newNovelForm: FormGroup; - - constructor(private fb: FormBuilder) { - this.newNovelForm = this.fb.group({ - novelName: 'Zenith', - authors: this.fb.array([ - this.fb.group({ - name: 'Sasha Alsberg' - }) - ]) - }); - } + newNovelForm = this.fb.group({ + novelName: 'Zenith', + authors: this.fb.array([ + this.fb.group({ + name: 'Sasha Alsberg' + }) + ]) + }); + + constructor(private fb: FormBuilder) {} onSubmit() { // @@ -216,3 +216,51 @@ store.dispatch( }) ); ``` + +### Debouncing + +The `ngxsFormDebounce` is used alongside `debounceTime` and pipes form's `valueChanges` and `statusChanges`. This implies that state updates are asynchronous by default. Suppose you dispatch the `UpdateFormValue`, which should patch the form value. In that case, you won't get the updated state immediately because the `debounceTime` is set to `100` by default. Given the following example: + +```ts +interface NovelsStateModel { + newNovelForm: { + model?: { + novelName: string; + paperBound: boolean; + }; + }; +} + +export class NovelsState { + @Action(SubmitNovelsForm) + submitNovelsForm(ctx: StateContext) { + console.log(ctx.getState().newNovelForm.model); + + ctx.dispatch( + new UpdateFormValue({ + value: { paperBound: true }, + path: 'novels.newNovelForm' + }) + ); + + console.log(ctx.getState().newNovelForm.model); + } +} +``` + +You may expect to see `{ paperBound: true, novelName: null }` being logged. Still, the second `console.log` will log `{ paperBound: true }`, pretending the `novelName` value is lost. You'll see the final update state if you wrap the second `console.log` into a `setTimeout`: + +```ts +ctx.dispatch( + new UpdateFormValue({ + value: { paperBound: true }, + path: 'novels.newNovelForm' + }) +); + +setTimeout(() => { + console.log(ctx.getState().newNovelForm.model); +}, 100); +``` + +If you need to get state updates synchronously, you may want to set the `ngxsFormDebounce` to `-1`; this won't pipe value changes with `debounceTime`. diff --git a/packages/form-plugin/src/directive.ts b/packages/form-plugin/src/directive.ts index 6cac8df6c..2297f3af2 100644 --- a/packages/form-plugin/src/directive.ts +++ b/packages/form-plugin/src/directive.ts @@ -18,21 +18,26 @@ export class FormDirective implements OnInit, OnDestroy { path: string = null!; @Input('ngxsFormDebounce') - debounce = 100; + set debounce(debounce: string | number) { + this._debounce = Number(debounce); + } + get debounce() { + return this._debounce; + } + private _debounce = 100; @Input('ngxsFormClearOnDestroy') set clearDestroy(val: boolean) { this._clearDestroy = val != null && `${val}` !== 'false'; } - get clearDestroy(): boolean { return this._clearDestroy; } + private _clearDestroy = false; - _clearDestroy = false; + private _updating = false; private readonly _destroy$ = new Subject(); - private _updating = false; constructor( private _actions$: Actions, @@ -167,9 +172,9 @@ export class FormDirective implements OnInit, OnDestroy { complete: () => (this._updating = false) }); } + ngOnDestroy() { this._destroy$.next(); - this._destroy$.complete(); if (this.clearDestroy) { this._store.dispatch( @@ -186,12 +191,12 @@ export class FormDirective implements OnInit, OnDestroy { private debounceChange() { const skipDebounceTime = - this._formGroupDirective.control.updateOn !== 'change' || this.debounce < 0; + this._formGroupDirective.control.updateOn !== 'change' || this._debounce < 0; return skipDebounceTime ? (change: Observable) => change.pipe(takeUntil(this._destroy$)) : (change: Observable) => - change.pipe(debounceTime(this.debounce), takeUntil(this._destroy$)); + change.pipe(debounceTime(this._debounce), takeUntil(this._destroy$)); } private get form(): FormGroup { diff --git a/packages/form-plugin/tests/form.plugin.spec.ts b/packages/form-plugin/tests/form.plugin.spec.ts index 56d254b95..5fc48f0af 100644 --- a/packages/form-plugin/tests/form.plugin.spec.ts +++ b/packages/form-plugin/tests/form.plugin.spec.ts @@ -38,6 +38,7 @@ describe('NgxsFormPlugin', () => { } } }) + @Injectable() class StudentState { @Selector() static getStudentForm(state: StudentStateModel): Form { @@ -316,6 +317,7 @@ describe('NgxsFormPlugin', () => { } } }) + @Injectable() class TodosState {} TestBed.configureTestingModule({ @@ -612,7 +614,7 @@ describe('NgxsFormPlugin', () => { @Component({ template: ` -
+
` @@ -798,7 +800,7 @@ describe('NgxsFormPlugin', () => { @Component({ template: ` -
+
` diff --git a/packages/form-plugin/tests/issues/issue-1590-update-form-value-primitive.spec.ts b/packages/form-plugin/tests/issues/issue-1590-update-form-value-primitive.spec.ts index cbc2ff2e1..f0a03cf2c 100644 --- a/packages/form-plugin/tests/issues/issue-1590-update-form-value-primitive.spec.ts +++ b/packages/form-plugin/tests/issues/issue-1590-update-form-value-primitive.spec.ts @@ -1,3 +1,4 @@ +import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { NgxsModule, Selector, State, Store } from '@ngxs/store'; @@ -21,6 +22,7 @@ describe('UpdateFormValue with primitives (https://github.com/ngxs/store/issues/ } } }) + @Injectable() class PizzaState { @Selector() static getModel(state: PizzaStateModel) { diff --git a/packages/form-plugin/tests/issues/issue-1822-multiple-ngxs-form-bindings.spec.ts b/packages/form-plugin/tests/issues/issue-1822-multiple-ngxs-form-bindings.spec.ts index 7d091f20f..66ab61387 100644 --- a/packages/form-plugin/tests/issues/issue-1822-multiple-ngxs-form-bindings.spec.ts +++ b/packages/form-plugin/tests/issues/issue-1822-multiple-ngxs-form-bindings.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { Component } from '@angular/core'; +import { Component, Injectable } from '@angular/core'; import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; import { NgxsModule, State, Actions, ofActionDispatched, Store, Selector } from '@ngxs/store'; @@ -26,6 +26,7 @@ describe('Multiple `ngxsForm` bindings (https://github.com/ngxs/store/issues/182 } } }) + @Injectable() class UserState { @Selector() static getModel(state: UserStateModel) { diff --git a/packages/form-plugin/tests/property-path.spec.ts b/packages/form-plugin/tests/property-path.spec.ts index 3dcd9776c..f64b5f916 100644 --- a/packages/form-plugin/tests/property-path.spec.ts +++ b/packages/form-plugin/tests/property-path.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { Component } from '@angular/core'; +import { Component, Injectable } from '@angular/core'; import { By } from '@angular/platform-browser'; import { FormGroup, FormControl, FormArray, ReactiveFormsModule } from '@angular/forms'; import { State, NgxsModule, Store, Selector } from '@ngxs/store'; @@ -29,6 +29,7 @@ describe('UpdateFormValue.propertyPath', () => { } } }) + @Injectable() class NovelsState { @Selector() static model(state: NovelsStateModel) {