Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(form-plugin): allow ngxsFormDebounce to be string #1972

Merged
merged 1 commit into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
78 changes: 63 additions & 15 deletions docs/plugins/form.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -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() {
//
Expand All @@ -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<NovelsStateModel>) {
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`.
19 changes: 12 additions & 7 deletions packages/form-plugin/src/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>();
private _updating = false;

constructor(
private _actions$: Actions,
Expand Down Expand Up @@ -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(
Expand All @@ -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<any>) => change.pipe(takeUntil(this._destroy$))
: (change: Observable<any>) =>
change.pipe(debounceTime(this.debounce), takeUntil(this._destroy$));
change.pipe(debounceTime(this._debounce), takeUntil(this._destroy$));
}

private get form(): FormGroup {
Expand Down
6 changes: 4 additions & 2 deletions packages/form-plugin/tests/form.plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('NgxsFormPlugin', () => {
}
}
})
@Injectable()
class StudentState {
@Selector()
static getStudentForm(state: StudentStateModel): Form {
Expand Down Expand Up @@ -316,6 +317,7 @@ describe('NgxsFormPlugin', () => {
}
}
})
@Injectable()
class TodosState {}

TestBed.configureTestingModule({
Expand Down Expand Up @@ -612,7 +614,7 @@ describe('NgxsFormPlugin', () => {

@Component({
template: `
<form [formGroup]="form" ngxsForm="todos.todosForm" [ngxsFormDebounce]="-1">
<form [formGroup]="form" ngxsForm="todos.todosForm" ngxsFormDebounce="-1">
<input formControlName="text" /> <button type="submit">Add todo</button>
</form>
`
Expand Down Expand Up @@ -798,7 +800,7 @@ describe('NgxsFormPlugin', () => {

@Component({
template: `
<form [formGroup]="form" ngxsForm="todos.todosForm" [ngxsFormDebounce]="-1">
<form [formGroup]="form" ngxsForm="todos.todosForm" ngxsFormDebounce="-1">
<input formControlName="text" /> <button type="submit">Add todo</button>
</form>
`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { NgxsModule, Selector, State, Store } from '@ngxs/store';

Expand All @@ -21,6 +22,7 @@ describe('UpdateFormValue with primitives (https://github.com/ngxs/store/issues/
}
}
})
@Injectable()
class PizzaState {
@Selector()
static getModel(state: PizzaStateModel) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -26,6 +26,7 @@ describe('Multiple `ngxsForm` bindings (https://github.com/ngxs/store/issues/182
}
}
})
@Injectable()
class UserState {
@Selector()
static getModel(state: UserStateModel) {
Expand Down
3 changes: 2 additions & 1 deletion packages/form-plugin/tests/property-path.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,6 +29,7 @@ describe('UpdateFormValue.propertyPath', () => {
}
}
})
@Injectable()
class NovelsState {
@Selector()
static model(state: NovelsStateModel) {
Expand Down