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(proposal): 3 types of form event utilities (observable streams / type filters / signal values) #376

Closed
michael-small opened this issue May 19, 2024 · 3 comments

Comments

@michael-small
Copy link
Contributor

michael-small commented May 19, 2024

edit: TL;DR

  1. value/status/pristine/touched all as observables, individual properties or maybe just all rolled into one observable. And there is always a starting value.
  2. Filters and predicates for types of events
  3. Basically toSignal() version of the stuff from # 1.

edit:
Stackblitz: https://stackblitz.com/edit/stackblitz-starters-nbicus?file=src%2Fform-events-utils.ts
PR: #391

(and reset/submitted are possible but kind of edge cases for reasons)


This is a concrete proposal building on a discussion that I opened before: #334. I will refer to this discussion for a link so I do not tag related issues in the Angular GitHub again.

Background to new API before I explain the utility(s) I propose

Edit: @TheIgorSedov just released a great video that explains the API very well with some good graphics and step by step explanations.

Unified Control State Change Events #54579 (see link in above discussion) introduces an observable on forms called events that exposes a stream of events and their respective values. It is targeted for Angular 18, and in my experimentation during release candidates, very useful. I believe that there is room for utilities to be made out of it.

Events + values of those events

  • value
  • status
  • touched
  • pristine
  • reset (no value accessor)
  • submitted (no value accessor)

I think my following proposals mirror existing ngxtension utilities and approaches (injectNavigationEnd and @joshuamorony usage of forms with signalSlice).

My proposal: 3 types of utilities

  1. Observable streams + values
  2. Instance type filters
  3. Signal values

Link to assorted utilities I have made with this new API. Reset/submitted haven't been merged into main yet as those are newer and kind of edge cases. For use cases, you can see some of them used in the main example app component.

Stackblitz: https://stackblitz.com/edit/stackblitz-starters-nbicus?file=src%2Fform-events-utils.ts

Type 1: Observable events + values

These could all have initial values passed into the streams with RXJS's startWith. I will give these examples as versions without that for now.

This was inspired by @joshuamorony's video on using signalSlice and form.valueChanges to subscribe to a form's values to get its state as a signal and do side effects. I have further worked on this since this recent issue #365. That issue is particular to the forms approach with signalSlice and the topic at hand has gotten down to a couple details, but the crux of my proposal is that form observables can make for great events and signals, and this new API exposes a lot we never had until Angular 18.

The generic for events in the unified form events API is of type ControlEvent. To get an event when the form's pristine state changed, it looks like this

form.events.pipe(
    filter(
         (event: ControlEvent): event is PristineChangeEvent =>
         event instanceof PristineChangeEvent
    )
)

An example of an event instance type utility would be a function that does something like the above, but used with this signature:

function $prisineEvents<T>(form: AbstractControl<T>)

Usage

pristineEvents$ = pristineEvents$(this.form).pipe(
    tap((p) => console.log('pristine events, map this out or whatever you want', p))
);

Since there is 6 event types (and possibly more in the future), it is nice to have them as easy to reference functions. In fact, I use them internally in a later example.

This functionality is somewhat like a simple version of an existing ngxtension utility, injectNavigationEnd. To the extent that filtering for the type NavigationEnd is a bit awkward to do without a utility compared to how useful and common it can be. I don't think this would work like the injectors though, as it has to pass in controls. But these functions do have their own injection context and all that. And all of these utilities would naturally be modified to handle injection contexts and potential leaks.

If it is too granular to have a utility for each of these, I have also made a combined stream that maps out by type. This one does use startWith to get initial values.

function allEventsUnified$<T>(form: AbstractControl<T>): Observable<{
    value: T;
    status: FormControlStatus;
    touched: boolean;
    pristine: boolean;
}>

Maybe it could be extended to optionally allow for submitted and reset, but those are only events and do not have values and thus would require some initial value or to track other things (Last submitted time? Number of submits? Not sure if it's worth it but it would be different).

Type 2: Type filters

Functions that assert what an event type is, with signatures such as

isPristineEvent(pristine)

Should someone want to grab the whole form.events stream and filter inside with ease. Not as useful in my opinion but it's something possible.

Type 3: Signal values

Naturally, I have toSignaled all of the observables as well. For example:

function $prisineEvents<T>(form: AbstractControl<T>): Signal<PristineChangeEvent | undefined>

To always have values, initial values can be passed in to the streams with the RXJS operator startWith. In the equivalent to allEventsUnified$ (which I call $allEventsUnified), all the types of events are combined and passed the respective form.pristine or the other event values.

Signature of $allEventsUnified

function $allEventsUnified<T>(form: AbstractControl<T>, formSubmitted?: boolean, formReset?: boolean): Signal<{
    value: T;
    status: FormControlStatus;
    touched: boolean;
    pristine: boolean;
} | {
    readonly value: T;
    readonly status: FormControlStatus;
    readonly pristine: boolean;
    readonly touched: boolean;
}>

As with the single observable stream, submitted and reset are a bit of an edge case.

@michael-small
Copy link
Contributor Author

I edited this into the issue about as well, but this is so good I want to highlight it here as well: https://www.youtube.com/watch?v=v7r-7PHaEtY

Igor Sedov made a great video that explains the new events API that this proposal builds on. It covers just about everything I wish I knew about this new form events API before I started making things with it.

@michael-small
Copy link
Contributor Author

michael-small commented May 25, 2024

I cannot get a Stackblitz going in v18 stable for the life of me. Anyone got one that works? I think there is some issue with node or the newer Angular builders but I was hoping I could demo this on v18 stable by the time it was out.

I guess the 18.0.0-rc1 example is fine for the most part minus the event type name changes and not having submit/reset events.

edit: context: https://x.com/MichaelSmallDev/status/1794451615916917059

  1. You can upgrade a v17 project to v18 using ng update
  2. There is a pending PR on Stackblitz side for Angular 18.0 support

edit 2: made a starter for Angular 18 + Material, will make example of these form utils without Material sometime later https://stackblitz.com/edit/stackblitz-starters-o2heai?file=package.json

@michael-small
Copy link
Contributor Author

#391 is now marked for review

@nartc nartc closed this as completed Aug 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants