angular-architects / ngrx-toolkit

Various Extensions for the NgRx Signal Store
MIT License
98 stars 16 forks source link

Add a form management feature to a signal store #45

Open ebondu opened 2 months ago

ebondu commented 2 months ago

When using complex forms in Angular, it could be convenient to handle the form management with a dedicated store feature.

Some commons scenarios where adding a withForm() feature to a signal store could be useful:

To connect the form to the store, a dedicated form directive should be used.

The approach could look like this:

In template, using the appSignalStoreForm directive :

<div [formGroup]="myForm" appSignalStoreForm [store]="myStore" formFeatureName="mySearch">
            <select formControlName="period">...</select>
            <input formControlName="from">
            <input formControlName="to">
...
</div>

In the component, nothing required except the store:

export class SearchComponent {
    readonly myStore = inject(MyStore);
    ...
}

In the store, declaring the withForm feature and the required computed and method :

export const MyStore = signalStore(
    withForm('mySearch'),
    withComputed(({ mySearchFormState }) => {
        return {
            // method called by the form directive to compute changes on form changes
            mySearchFormComputedChanges: computed<ControlChange[]>(() => {
                const changes: ControlChange[] = [];

                // here is the business logic to update the form as a side effect 
                if (mySearchFormState().controlsMarkedAsChanged.some((control) => control === 'period')) {
                    if (mySearchFormState().value.period === 'current') {
                        changes.push({ controlName: 'from', operation: new SetValue(now()) });
                        ...
                    }
                }
                // the list of changes the form directive will apply on the form
                return changes;
            }),
        };
    }),
    withMethods((store) => ({
        // method called by the directive after changes applied
        handleMySearchForm() {
           // using the form values representation to do something
           search(store.mySearchFormState().value?.from, store.mySearchFormState().value?.to);
        }
    }))

Let me know if you think this is feature could be relevant for the project.

rainerhahnekamp commented 2 months ago

Hello, that sounds very interesting, and we should dig deeper here. Some initial thoughts:

Last question: Who's going to implement it? :)

ebondu commented 2 months ago

Hello, thanks for your feedback.

What if we use the Signal coming from @ViewChild and pass that one? So the SignalStore would provide instead a setter method where it gets the FormGroup? That would mean that the SignalStore is also initialized without form. That would not require any changes in the template, and we wouldn't have to come up with directives, etc.

I was thinking to a directive because it looked to be the simplest place to make the link between the formGroup and the store. Also, it required few changes in the template and no change in the component, but maybe the setter approach is simplest at the end. The logic in the directive to handle the form change / path it + calling store methods to patch the state should also works in the feature itself? A generated method set{name}FormGroup(FormGroup) could be added to the store, containing the form + state management logic. Named setter methods seems required to allow multiple use of the withForm feature in the same store.

We know that Angular plans to integrate Signals into the ReactiveFormsModule before they start working on a completely Signal-based form feature. Do you think we should wait for what they bring out?

I looked for news about this but the roadmap and the approach is not clear to me. At the end the angular approach will not be related to ngrx signal store so the feature looks to be relevant to me for developers that will use ngrx signal store.

I had also considered form support in the past, but I was more inclined towards template-driven forms. In the end, if you could manage that the SignalStore doesn't just provide DeepSignal but something like a DeepWritableSignal, then you could apply the slices directly via two-way binding to the ngModel. That could be a separate extension, maybe.

I didn't consider this way but you are right the DeepWritableSignal could change the game

Have you checked https://github.com/timdeschryver/ng-signal-forms? Maybe we can learn from this project or make it compatible.

I looked this project but there is nothing related to side effect management and also the declaration for each input implies more changes in the templates.

I got a first implementation (poc) but with all the form + state management logic in the directive, not in the feature itself. I can investigate the setter approach and also share the first draft if you want

ebondu commented 2 months ago

I confirm that your idea of using a setter to pass the formGroup to a feature method works ;) So no need to add a directive anymore because all the logic is now in the feature. But there is also a counterpart: I must pass the store as a second parameter of the setter :

ngOnInit(): void {
   this.store.setMySearchFormGroup(this.mySearchForm, this.store);
}

The reason is that the accessible store in the context of the feature method is the temp store, containing only methods already declared/generated before the call to the withForm('mySearch'). By passing the store as a second parameter, the accessible store in the feature method is the final one, containing all required computed methods. Do you have an idea to prevent this extra parameter ?

rainerhahnekamp commented 1 month ago

I do have an idea but I'm not sure if it is a good one :)

I was thinking of initialzing the signalStore with a dummy FormGroup which throws an error if accessed. It would be like a mock in testing.

ebondu commented 1 month ago

Hi, the access error is more on methods required by the feature that are declared after rather than the formGroup object. Maybe my question was not clear.

I also create the PR if you want to have a look on the draft.