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

Signals for Reactive and Template-based Forms #53485

Open
gionkunz opened this issue Dec 10, 2023 · 2 comments
Open

Signals for Reactive and Template-based Forms #53485

gionkunz opened this issue Dec 10, 2023 · 2 comments

Comments

@gionkunz
Copy link
Contributor

gionkunz commented Dec 10, 2023

Which @angular/* package(s) are relevant/related to the feature request?

forms

Description

While the current forms module already supports reactivity using valueChanges and statusChanges and we can use RxJS / Signal interoperability helpers to work with signals in our reactive forms, there's still quite a gap when it comes to form handling with the new signal API.

While I believe that a complete rewrite of the Forms Module that is more aligned to the simplistic API design of signals, inject helper and signal-based component helper and is also purely based on signals for any form state, I also think that this requires a lot of time to rethink and build. I like the proposal of @ShacharHarshuv #51786, and it provides some good ideas about a simplistic API aligned to signals. However, there are still many things to consider (custom control values that are non-primitives and can cause confusion with groups (TValue first vs TControl first discussion in Typed forms discussion).

In the meantime, it would be great to have some updates to the existing form modules that would allow us to work with signal-based state when handling forms. Also, it would be great to expose any form-based state as reactive values contrary to the sometimes limited valueChanges and statusChanges. This would allow the combination of form state using computed and the execution of side-effects using effect to fulfil many form handling use cases.

Proposed solution

The proposed solution is to include signals for all relevant form states within AbstractControl and update them whenever the local state of the form controls gets updated. This way, developers can benefit from the new reactive form state in both reactive and template-driven forms since NgForm / NgModel uses AbstractControl under the hood. This is a feasible and non-intrusive alternative before a possible rewrite of the Form Module.

Within this proposal, the signals of AbstractControl that represent the form controls state are grouped within a new signals object inside AbstractControl. This allows the regular form state and signals to coexist without causing name clashes or overcomplicated lengthy prefixes. This also encourages the extraction of specific signals using destructuring const {value, valid} = myControl.signals;.

For template-driven forms using NgForm and NgModel, the current idea is to provide a way to inject your signals from your component into the controls using ngFormOptions and ngModelOptions. Since the developer does not have control over the form control creation in template-driven forms, that's the simplest way to connect forms created within the template to some signals within your component. An alternative idea would be to obtain the NgModel directive within the component using ViewChild and then receive the signals from the exposed control. However, that would require you to wait for AfterViewInit or even AfterViewChecked, and this would compromise the declarative approach, which is possible by passing manually created signals into the controls.

To provide the necessary convenience to use external form control signals, I've added a helper function, createFormSignals. In the current version of the PR, I'm also experimenting with making the value signal a WritableSignal. This way, in the most simple way to use this proposal in conjunction with NgModel / NgForm, the value signal can be used to update the form value programmatically from within the component. Currently, this is done via an effect inside abstract control, and a lot of testing needs to be done to make this robust enough and ensure a consistent state while not causing any loops.

@Component({
  selector: 'app-capacity',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <p>Reservations: {{reservations()}}</p>
    <p>Capacity: <input type="number" [formControl]="capacity" /></p>
  `
})
export class CapacityComponent {
  capacity = new FormControl(5, {nonNullable: true});
  reservations = signal(0);
  interval = setInterval(() => this.reservations.update(count => count + 1), 1000);
  hasCapacity = computed(() => this.capacity.signals.value() > this.reservations());
  capacityEffect = effect(() => console.log(this.hasCapacity() ? 'We have some capacity!' : 'No capacity!'));
}

This simple example with reactive forms shows that even when FormControl does not natively use signals, we still benefit from the newly exposed signals object and can create larger reactive constructs quite declaratively.

With template-driven forms, we can achieve similar reactive handling by using the ngFormOptions and ngModelOptions directive inputs. Since NgModel or NgForm model binding is optional, the existing template-driven forms can become the default tool when creating signal-based forms with this proposal.

@Component({
  selector: 'app-simple-template',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input type="text" reqired ngModel [ngModelOptions]="{signals: searchQuery}" />
    <div>Results: {{results()}}</div>
  `
})
export class SimpleTemplateComponent {
  searchService = inject(SearchService);
  searchQuery = createFormSignals('');
  results = signal([] as string[]);
  searchEffect = effect(async () => {
    if (this.searchQuery.value().length > 3) {
      this.results.set(await lastValueFrom(this.searchService.search(this.searchQuery.value())));
      // Use the writable value signal to update the form programmatically (currently experimenting with this)
      this.searchQuery.value.set('');
    }
  }, {allowSignalWrites: true});
}

Or a little more complex template-driven form:

@Component({
  selector: 'app-signal-based-form',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <form #form="ngForm" [ngFormOptions]="{signals}">
      <label>
        First Name:
        <input type="text" required name="firstName" ngModel/>
      </label>
      <label>
        Last Name:
        <input type="text" name="lastName" ngModel/>
      </label>

      <p>Full name: {{ fullName() }}</p>
      @if (showErrors()) {
        <p>Form contains errors</p>
      }
      <button (click)="updateFormValue()">Update form</button>
    </form>
  `,
})
export class SignalBasedFormComponent {
  signals = createFormSignals({ firstName: '', lastName: '' });
  fullName = computed(
    () => `${this.signals.value().firstName} ${this.signals.value().lastName}`
  );
  showErrors = computed(
    () => this.signals.invalid() && this.signals.dirty()
  );
  logFullname = effect(() => {
    console.log(`Full name: ${this.fullName()}`);
  });
  updateFormValue() {
    this.signals.value.set({
      firstName: 'Bob',
      lastName: 'Smith'
    });
  }
}

I've created a draft PR with the changes required for this new feature. The draft PR does not contain any tests, no documentation and probably has a few bugs. #53481

I've also published the forms module containing those changes to a temporary NPM package and included it in a stackblitz for you to play around: https://stackblitz.com/edit/stackblitz-starters-uyjg2x?file=src%2Freactive-form.component.ts,src%2Ftemplate-driven-form-signals-option.component.ts,src%2Ftemplate-driven-form.component.ts,src%2Fsignal-based-forms.component.ts

I would love to hear your feedback, thoughts and ideas!

Cheers
Gion

Alternatives considered

n/a

@ShacharHarshuv
Copy link

I think it's a great way to ship the benefits of signals to Angular forms quickly. I also agree that there might be a place to completely rethink forms in Angular, and simplify the DX around them. (Like in my proposition)

@gionkunz
Copy link
Contributor Author

I have just updated the proposal to include a more streamlined way to create form signals for use in NgForm and NgModel. I've also experimentally included that value is now a WritableSignal on AbstractControlSignals. The examples are updated, and so are the NPM library and Stackblitz.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

No branches or pull requests

3 participants