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

docs: add Unified Form Events page #451

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions docs/src/content/contributors/michael-small.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Michael Small",
"github": "https://github.com/michael-small"
}
97 changes: 97 additions & 0 deletions docs/src/content/docs/utilities/Forms/unified-form-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: Unified Form Events
description: .
entryPoint: form-events
badge: stable
contributors: ['michael-small']
---

Unified Form Events expose as much reactive form state as possible, as both an observable or a signal.
Like reacting to `.valueChanges()` or `.statusChanges()`, this utility is based on the new [unified form events API](https://github.com/angular/angular/pull/54579) introduced in Angular 18.

`allEventsObservable` and `allEventsSignal` are exposed as an `Observable` and readonly `Signal`
with the following typing.

```ts
type FormEventData<T> = {
// So about this... see "Usage" to why this is in effect `Partial<T>`,
// and the last section for one approach to avoid this using `signalSlice`.
value: T;

status: FormControlStatus;
touched: boolean;
pristine: boolean;
valid: boolean;
invalid: boolean;
pending: boolean;
dirty: boolean;
untouched: boolean;
};
```

The form events API also exposes events for submitted and reset, but those are not included
in this utility as they do not have value accessors.

## Import

```typescript
import { allEventsObservable, allEventsSignal } from 'ngxtension/form-events';
```

## Usage

> DISCLAIMER:
> Due to some constraints of reactive forms, the value ends up resolving when implemented as `Partial<T>`. However, most if not all edge cases are accounted for, so in the author's opinion and personal use it should be safe to assert it as non-Partial as users see fit. See discussion in this utility's [pull request](https://github.com/ngxtension/ngxtension-platform/pull/391) and a [related issue](https://github.com/ngxtension/ngxtension-platform/issues/365). The author's favorite way to use this form utility and to type the value is explained in the last documentation subsection, ["Synergy with `signalSlice`"](#synergy-with-signalslice---removing-the-partial-from-partialt-and-more).

[Stackblitz example](https://stackblitz.com/edit/stackblitz-starters-masfsq?file=src%2Fform-events-utils.ts) of both functions.

### `allEventsObservable`

```ts
export class App {
fb = inject(NonNullableFormBuilder);
form = this.fb.group({
firstName: this.fb.control('', Validators.required),
lastName: this.fb.control(''),
});
form$ = allEventsObservable(this.form);
}
```

### `allEventsSignal`

```ts
export class App {
fb = inject(NonNullableFormBuilder);
form = this.fb.group({
firstName: this.fb.control('', Validators.required),
lastName: this.fb.control(''),
});
$form = allEventsSignal(this.form);
}
```

## Synergy with `signalSlice` - removing the `Partial` from `Partial<T>` and more

This form events utility was inspired by ngxtensions's [`signalSlice`](https://ngxtension.netlify.app/utilities/signals/signal-slice/) creator Josh Morony. Josh outlines this approach in his video ["A trick to make your Angular Reactive Forms more... _Reactive_"](https://www.youtube.com/watch?v=cxoew5rmwFM&t=211s). Rather than the `formValues` util that Josh provides in the `signalSlice` `sources` array, you can provide `allEventsObservable(this.form)`.

`signalSlice` does some type magic that allows the user to assert that the `Partial` from `Partial<T>` will only be `T` in effect. The typing of the `signalSlice` is infered from it's `initialState` parameter, and the value for type `T` can be specified with:

```ts
initialFormState = {
...this.form.getRawValue(), // `.getRawValue()` gets around the `Partial` limitiation
// ...
};

formState = signalSlice({
initialState: this.initialFormState, // The form's value will be `T`
sources: [allEventsObservable(this.form)],
// ...
});
```

This approach not only helps the typing of the form value, but also

- Other `sources` such as HTTP or store events. The slice's form value can be set using `this.form.patchValue` as a side effect in one or more of those mappings. [Example source code from Josh](https://github.com/joshuamorony/signal-slice-forms/blob/main/src/app/home/home.component.ts#L62).
- Allows imperative changes such as form submissions using `actionSources`
- `selectors`, aka built in `computed` values accesible directly from the slice.
Loading