Repository: angular
Issue: Proposition: Alternative, Simplified Forms API
Labels: area: forms

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

_No response_

### Description

**Update:** I have an (almost complete) implementation of this proposition: https://github.com/ShacharHarshuv/formal, with some API differences that I've made along the way. 

I realize this might not be the "standard" feature request, but I'm not sure of a better place to share it and start a serious discussion. 

Angular's `ReactiveForms` module has revolutionized the way we think and work with form management. However, after around 5 years of working with it, and in lights of the recent changes going around in the Angular community, I believe it might be a good time to reconsider the future of managing forms in Angular. 

 The main things that I would like to improve is simplify the API, reduce boilerplate code, make the API for managing different kind of states (disabling, validation) more consistent (aka declarative vs imperative, `ControlValueAccessor` managed vs CSS managed etc), make it more open to extension with the entire form logic in one place etc, and - if we are already revisiting it - make use of a more modern API (signals). 
 
As a first step, I've created a design proposition for what this could be. I believe this could be both easier to work with and more flexible, compared to Angular's current solution. 

The purpose of this post is to get some initial feedback, start the conversation, and see if other people are interested in this direction. 

### Proposed solution

Before we start, here are some high level goals for this design:

- Easy initialization
    - Define logic in one place
- Easy binding / property accessing
- Close for modification and extendable (open-close)
- Minimal
- Type safety
    - Including in the template! So you'll not be able to use a CVA component with the wrong control.
- Separation of logic (value, disability, validity)
- Modern API (signals)
- Easier access of (custom) state (aka, not just built in disabled / validation) by custom form components (or their children). (This relates to the usage in the template more than anything else). (For example, if we want to show a component differently if it's required)

# Proposition

We'll have a single type (not class) called `Form`. This type will be used for what `FormControl`, `FormGroup` and `FormArray` were used before.

Each instance of `Form` will enable two way binding, listening to changes (via signals API), and plugging in of other state like disability and validation. The other state will be separated and pluggable so it will be easy to extend it in a way that is open-close.

## Core Behavior

### Creating a `Form`

Create a form is as easy is passing the default value to the `form` factory function:

```ts
const name = form('Sweeney');
```

The type is inferred to be `Form<string>`. You can also be explicitly set the type (to be more specific or more generic):

```ts
const name = form<string>('Sweeney');
```

Reading/listening to the value can be done like you would with regular signals:

```ts
console.log('name', name());
```

> [!note]
> We need to make sure that the value that is returned from the form this way is readonly, to avoid a situation where a consumer can mess with it. It might be the case that it works ootb with signals, but we need to check.

Setting the value can be done with any of the `WritableSignal` methods (`set`, `update` or `mutate`):

```ts
name.set('Ms. Lovett');
```

> [!warning]
> Even though to the consumer the form looks like a signal (and it's assignable to its interface), it will actually need to be implemented differently, even though it will use the signals infra behind the scenes.

### Nested forms

When your `Form` has a generic that is either an object or an array, you'll also have the `fields` signal available on it as a property.

> [!note]
> If the generic is a union type that can be either an object or something else, the `fields` property will not exist

```ts
const myForm = form({
  name: "Sweeney",
  age: 42,
});
myForm.fields().name.set("Sweeney Todd");
```

When you are creating a form in this way, you can also pass the `Form` objects directly as properties, instead of as values. This can be useful if you want to set different validators / disabled state / type.

```ts
const myForm = form({
  name: form("Sweeney"),
  age: form(42),
});
```

When used in this way, the inferred value will still be `{ name: string, age: number }`

This works the same with an array:

```ts
const characters = form(['Sweeney', 'Ms. Lovett']);
```

This is similar to `ReactiveForms`'s `FormArray`.

When setting value to the `form`, any listeners for the children events, will be updated.

```ts
const myForm = form({
  name: "Sweeney",
  age: 42,
});

effect(() => {
  console.log('name', myForm.fields().name())
});

myForm.set({ name: "Ms. Lovett", age: 38 }); // prints: name "Ms. Lovette"
```

When setting value to one of the children, any listeners to the form value will be updated.

```ts
const myForm = form({
  name: "Sweeney",
  age: 42,
});

effect(() => {
  console.log('form value', myForm());
});

myForm.fields().name.set('Sweeney Todd'); // prints: form value { name: "Sweeney Todd", age: 42}
```

When changing the value, you could also change the number of fields in the form. This is especially useful for arrays, but can be used in objects as well.

```ts
const myArray = form(['Shahar', 'Adi']);
myArray.mutate(array => array.push('Alma')); // this creates a new field
```

When listening to the `fields` signal change, it will only emit a change when the fields themselves are changing, not if one of the values are changing. Fields that ere not touched in this change, should have a reference to the same `Form` object.

This is useful if you have a `Form` with changing elements, like an array:

```html
<div *ngFor="let field of myForm.fields()">
	<input [form]="field" />
</div>
```

(In the example above we make use of the "form" directive)

## View Binding

In order to bind the new controls to the DOM, you can use the `[form]` directive like so:

```html
<input [form]="myFormField" />
```

This will work out of the box for native fields like `input`, and will be type-safe, so you can only pass it `Form<string>` if you're using it with an `input` of type text.
### Compatibility layer with reactive forms

The compatibility layer will be available under a separate library so it's optional. It has a peer dependency on Angular reactive forms, and - when imported - it enables binding forms to any component that provides `NG_VALUE_ACCESSOR`. So that you can use it with your custom components.

It will handle:

- Updating the value both ways (using `writeValue` and `registerOnchange`)
- Updating the touched state.
- Updating the disabled state.

### API for implementing custom components

Goals:

- Type safety in templates (for custom components)
- Ease of development

To implement a custom component, you need to extend this class:

```ts
class FormFieldComponent<T> {
  @Input('form')
  field: Form<T>
}
```

#todo _we might want to create a different API here for signal components' inputs. We might also enable both options._ _If we can create a functional API, it's probably better_

This enables type safety in the template.

## Disabled State

"disabled" is a boolean state that is conventionally used to signal that a field cannot be edited by the user. However, it doesn't mean that you can't programmatically change it.

To avoid contradictions, and to encourage a declarative approach, it will not be possible to change the disabled state imperatively.

To define the state of the disabled state of a form control, use the `disabled` function:

```ts
function disabled(state: Signal<boolean | string | { toString?: () => string}>)
```

(string is a user friendly disabled _reason_)

Example:

```ts
const isDisabled = computed(...);
const nameForm = form('Shahar', disabled(isDisabled);
```

To read the disabled state:

```ts
isDisabled(myForm);
```

This uses a signal behind the scenes, so you can use it to listen for changes inside an `effect` or `compute`:

```ts
effect(() => {
  console.log('form is disabled changed to', isDisabled(myForm));
})
```

In order to read it from the template, you can use a pipe:

```angular
Disabled: {{ myForm | formIsDisabled }}
```

All pipes will be standalones and need to be imported.

You can also set the initial disabled state of the form this way:

You can read the disabled reason with those functions:

```ts
firstDisabledReason(myForm); // will return null if not disabled

disabledReasons(myForm); // return string[] of all reasons
```

You can also use this in the template with the `formFirstDisabledReason` and `formDisabledReasons` pipes.

### View binding

By default when importing the `FormsModule` and using the `[form]` directive, a `disabled` attribute will be added to the host.

> [!question]
> Is it possible that a consumer will not want it? Do we need to be able to disable this behavior?
> 
> If needed, we can make this configurable in the `forRoot` options of `FormsModule`

> [!note]- Implementation Note
> This will be done by creating a `FormDisabledDirective` with the same selector that handles that.

## Dirty State

"dirty" is a Boolean state that is conventionally used to mark that a field was edited by the user. Common use-cases include:

- Show invalid indication only when dirty
- Enable save only when form is dirty

Reading the value:

```ts
isDirty(name)
```

This uses signal behind the scenes, so it can be listened to

Setting the value:

```ts
setIsDirty(name, false)
```

Even though it is used to show that the field was edited by the user, using `set` on a field does not automatically make it's "dirty" state true. You need to programmatically set the value to signal a user-invoked change:

```ts
name.set('Mr.' + lastName);
setIsDirty(name, true);
```

When binding fields to components, we'll use a dedicate directive (see later)

## Touched State

"touched" is a boolean state that is conventionally used to mark that a field was touched (but not necessary edited) by the user.

- Show invalid indication only when dirty

This has the same API as dirty state.

The compatibility layer with reactive forms handles setting the touched state when using the directive with a component that provides `NG_VALUE_ACCESSOR`.

## Validation

To add a validation to the form, you can do this:

```ts
form('', validation(required));
```

Since signals allow for a reactive API that can use any state, validators will be immutable, that means that contrary to `ReactiveForms`'s built in `setValidators`, in this library we cannot change the validators of a field after we've created it.

You can add multiple validations this way:

```ts
form('', validation(required, min(3)));
```

This can be used together with the `disabled` state this way:

```ts
form('', validation(required), disabled(true));
```

> [!note] Order doesn't matter
> You can also add the disabled state before the validation state (order doesn't matter)

To read the validation state of the form, you can use the following functions:

```ts
function isValid(form: Form): boolean;
function isInvalid(form: Form): boolean;
```

To get a user-friendly error message for the validation, you can use these functions:

```ts
function firstErrorMessage(field: Field): string | null; // null if valid or no error message available
function errorMessages(field: Field): string[];
```

All of these use signal behind the scenes, so they can be used in a reactive context.

### Form Status

To read the validation state, use any of these functions:

```ts
formStatus(myForm);
```

This returns `'valid'` | `'invalid'` | `'pending'` | `'disabled'`. The `disabled` status is based on the disabled state.

### View Binding

By default when importing the `FormsModule` and using the `[form]` directive, it will add `invalid`, `valid`, `validation-pending` classes based on the state of the validation of the control.

> [!question]
> Should we prefix these names to avoid collisions?
>
> Is it even a good idea? Maybe we want this only for native elements?
>
> Is it possible that consumers will want to disable this behavior?

These classes can be used globally like `ng-invalid` to add styling to your components.

This will be accomplished by a dedicated `FormValidationDirective`

### Customize Error Messages for built-in validators

To customize error messages of build in validators you can use the following API:

```ts
required.errorMessage("Required Field!"); // when passing a string here, the error message will always be the same
min.errorMessage(({min}) => `Value needs to be bigger than ${min}`); // use a callback to read data from the validator
```

(These callbacks are typed, so you can be sure you're reading the correct error type)

> [!note]- About implementation
> We might want to have some sort of utility function with that wraps those validators and enables this functionality. We can also expose this utility function to the public API so consumers can use it.

### Custom Validations

In order to add validation to your form, you need to define a `Validator`:

```ts
type ValidationError = string | {
	toString?: () => string;
}
type ValidationFn<Value> = (form: Form<Value>) => Signal<ValidationError | null>;
type Validator<Value> = ValidationFn<Value, Errors> | Signal<ValidationError>;
```

Here's a simple example of a validator

```ts
function required(form: Form<any>) {
 return computed(() => form.value() ? null : `This field is required`);
}
```

Unlike `ReactiveForms`, this validation function will be called _once_. Note that the form in the argument is there for convenience and reusability, but you can use any signal when creating it.

This set up makes it easier to write custom validators, that use different fields as a condition to the validation:

```ts
const isNameRequired = field(false);

const myForm = form({
 name: field('', (name) => {
   return isNameRequired.value() && !name.value() ? { required: true } : null
 }),
 isNameRequired,
});
```

The validation function can be strictly typed, so that this will not be allowed:

```ts
function min(form: Form<number>): Signal<ValidationErrors | null>;

form('', validation(min)); // ERROR: the validation type does not match the form type
```

The validation function can be strictly typed, so that this will not be allowed:

```ts
function min(form: Form<number>): Signal<ValidationErrors | null>;

form('', validation(min)); // ERROR: the validation type does not match the form type
```

For the error, you can return a string that will be used as a validation error message. If have other code that needs to read the error message content, you can also return an object with a `toString` method, that returns the error message. If you don't supply the `toString`, you might get `null` if you try to read error messages.

### "Async" Validators

Sometimes, validators need to run asynchronously. For that, you can use the `PENDING` symbol:

```ts
isNameUsedValidator(form: Form<string>) {
  const name$ = toObservable(form);

  const validationErrors$ = name$
    .pipe(
		switchMap(
			name => http.get('/is_available/${name}')
				.pipe(startWith(PENDING)) // every name change we need the PENDIG state
			),
		map(isAvailable => isVailable ? null : {taken: true}),
    );

	return toSignal(name$, PENDING);
}
```

#### Running Validation Lazily

Contrary to `ReactiveForms`, all validations will always be run together. If you have validation that are potentially expensive on calculation / network, and you don't want to run them if you don't have to (for example, when there are other errored validations), you need to explicitly ask for it:

```ts
form('', validations(
	required,
	runOnlyIfNeeded(isNameUsedValidator),
))
```

> [!note]- About implementation
>
> We'll use the attribute of signals that they are lazily evaluated. In the `compute` that calculates the validation errors, we need to ask for the "runOnlyIfNeeded" ones only if there aren't any other validators:
>
> ```ts
> const validationErrors = compute(() => {
>   const validationErrors = eagerValidationError.map(validationErrors => validationErrors());
>
>   if (!validationErrors.length) {
>     for (const lazyValidationError of lazyValidationErrors) {
>       if (lazyValidationError()) {
> 	      return [lazyValidationError];
>       }
>     }
>   }
>
>   return  validationErrors;
> })
> ```
>
> Note - this will still run all lazy validations together. Might want to rewrite it so that it also returns all errors

### Supporting NG_VALIDATOR

This feature in `Angular`'s `ReactiveForms` enables you to create a built-in validator inside a custom form control, which will cause it to be invalid and/or have errors. This uses `setValidators` behind the scenes.

There are a few issues with this approach in `ReactiveForms`: 

- Using `setValidators` does not automatically run the validations again, so the control might not get invalid even though it should be.
- If the custom control component was destroyed, the validator is removed. If the control was previously invalid, it might be become valid again when running update value and validity. This is usually not the desire behavior, since this case occurs usually when you hide views from the user, and you don't really want logic to change as a result. (For example, if you have a form that is divided to multiple routes)

To support NG_VALIDATOR, we'll have an *internal* function that can add errors (not validators) to the validation state of the control. 

We'll create `FormCustomValidatorDirective` that will use this internal function in hook into the `NG_VALIDATORS` provider to make this work.

> [!question]
> - Do we want this only for the compatibility layer, or do we also want to supply our own API for this?

### Error Aggregation

It's also sometimes useful to display all the errors of the form in one place. This should be possible to create utility function that aggregate errors. We might also want to add the functionality to add a display name to the form that will be used here. For example:

```ts
const firstName = form('',
  displayName('First Name')
);
```

### Binding Validation Information to View

A common use case, is to display an Asterix next to a required field label, or a message bellow a field. We would ideally want to implement this behavior once for our design system, so that it will work with all custom controls. 

There are two possible architectures here:

Field wrapper:

```html
<!-- consumer.html -->
<app-form-field>
  <app-form-label>Name</app-form-label>
  <app-text-field [form]="nameForm"/>
</app-form-field>
```

Label inside the field:

```html
<!-- consumer.html -->
<app-text-field 
   label="Name"
   [form]="nameForm"
/>

<!-- text-field.component.html -->
<app-field-frame>
   <app-form-label>{{label}}</app-form-label>
   ... <!-- text field implementation -->
</app-field-frame>

```

The difference is that in the first use case the form directive is used as a child of the component in which we want to access it form, and the second use-case the form directive is a parent of the component we want to access it from.

#### Accessing form of parent

To access the form of a parent, you can use the utility `injectForm()`. It will return a `Form` object if there is a parent that uses it. If there is more than one, it will return the first one from the bottom. 

Now, for example, if you want to check whether the form is required, you can do this:

```ts
readonly isRequired = computed(() => {
  return getFormValidators(this.form).includes(required);
});
```

#### Accessing form of child

To access the form of a child, you can add the `FormAccessorDirective` as a host directive of your wrapper component. This directive uses `@ContentChild` behind the scenes, and this way you can use the function `injectChildForm` to get the form of the first child.

---
## Custom State

We can extend the behavior of the form controls with custom state using the function `createFormState`. Many of the form states described before use it behind the scenes.

```ts
function createFormState<T, Args extends any[]>(
  descr: string,
  fallback: T,
  createInitialState: (form: Form<any>, ...args: Args) => T),
): {
  create: (...args: Args) => StateFactory, 
  get: (form: Form<any>) => T, // returns the fallback if doesn't exist
  set: (form: Form<any>, state: T) => void,
}
```

Here's an example of how you can use it:

```ts

// display-name.ts

export const {
  create: displayName, 
  get: getDisplayName,
  set: setDisplayName
} = createFormState(
  "Display Name",
  "",
  (form: Form<any>, displayName: string) => {
    return displayName;
  }
);

// my-form.ts

import { displayName, getDisplayName } from './my-custom-disabled-state';

const myForm = form({
  name: "John",
  age: 42,
}, displayName('Person'));

const displayNamed = getDisplayName(myForm);

```

This infra will also be used behind the scenes to support Disabled State, Dirty State]] and Validation.

Note that as other utilities, the getter uses signal behind the scenes so you can listen to changes as well. 



### Alternatives considered

N/A