DEV Community

Cover image for Signals Form: Introduction
Nicolas Frizzarin for This is Angular

Posted on

Signals Form: Introduction

Introduction and Disclaimer

First and foremost, the APIs that will be mentioned in this article are still highly experimental and may be subject to change in the future. As with all experimental APIs, I do not recommend using them in an application that is scheduled to go into production in the near future.

Today, and to no great surprise, the Angular Team is trying to incorporate signals as much as possible into existing APIs. Naturally, forms aren't being left behind, and a new type of form is emerging: Signal forms. This addition brings the total number of possible forms in Angular to three:

  • React Form: forms controlled by the component
  • Template Driven Form: forms controlled by the template
  • Signal Form: forms controlled by signals

Mindset of Signal Forms

No matter the framework or library you use to create a form, a form is simply a set of UI fields that allow the user to enter structured data, which can be governed by validation rules to ensure its integrity.

With Signal forms, this concept is separated into four distinct parts:

  • Data model: the form's current data model
  • Field State: the metadata associated with a form field (value, validity, and control state)
  • Field logic: the business logic of the field (validation rules, conditional display of the field)
  • UI Control: the control that enables interaction between native elements, custom components, and the user.

One of the key points of Signal's form is that it doesn't maintain its own data internally. Instead, we, the developers, expose this data model through a signal that the library will use as the uniq source for the fields of our form.

To illustrate this in code :)

interface Assigned {
  name: string;
  firstname: string;
}

interface Todo {
  title: string;
  description: string;
  status: TodoStatus;
  assigned: Assigned[];
}


@Component({
  selector: 'app-form',
  templateUrl: './app-form.html'
})
export class AppForm {
  todoModel = signal<Todo>({ 
    title: '',
    description: '', 
    status: 'not_begin',
    assigned: []
  }); // We create the model that will be the source of truth for the form and it's tree field

  todoForm = form(this.todoModel); // we create the form which is linked to the model

}
Enter fullscreen mode Exit fullscreen mode

Establishing the model as the single source of truth means two important things:

  • Any modifications to the model, via the set or update function, will automatically update the form field.
  • Any modifications to the form due to user interaction with a field will update the model.

A tree of Field

Calling the form function gives the developer access to a Field tree. The form itself is a Field called the Root Field.

A Field instance provides its state, which later allows you to retrieve its value, validity, and more and it can be retrieve by calling the Field function.

Let's illustrate that with a bit of code :)

interface Assigned {
  name: string;
  firstname: string;
}

interface Todo {
  title: string;
  description: string;
  status: TodoStatus;
  assigned: Assigned[]
}


@Component({
  selector: 'app-form',
  templateUrl: './app-form.html'
})
export class AppForm {
  todoModel = signal<Todo>({ 
    title: '',
    description: '', 
    status: 'not_begin',
    assigned: []
  }); // We create the model that will be the source of truth for the form and it's tree field

  todoForm = form(this.todoModel); // we create the form which is linked to the model

  titleField: Field<string> = this.todoForm.title;
  firstAssigned: Field<Assigned> = this.todoForm.assigned[0];
  firstAssignedName: Field<string> = firstAssigned.name;

}
Enter fullscreen mode Exit fullscreen mode

Field Instance

As explained previously, a Field instance returns the state of that field. The state is composed of 6 main points.

  • value: A WritableSignal that allows you to read and write the value of a field.
  • errors: A signal for retrieving validation errors on the field.
  • valid: A signal for retrieving the field's validity.
  • disabled: A signal for retrieving whether the field is disabled.
  • touched: A signal to know if the user has interacted with the field or one of its children.
  • dirty: A signal to know if the field or one of its children is dirty.

For more details on what a field instance can offer, or to see the concrete implementation, please refer to the following

Let's illustrate that with a bit of code.

interface Assigned {
  name: string;
  firstname: string;
}

interface Todo {
  title: string;
  description: string;
  status: TodoStatus;
  assigned: Assigned[]
}


@Component({
  selector: 'app-form',
  template: `<button [disabled]="titleField().valid()">Submit</button>`
})
export class AppForm {
  todoModel = signal<Todo>({ 
    title: '',
    description: '', 
    status: 'not_begin',
    assigned: []
  }); // We create the model that will be the source of truth for the form and it's tree field

  todoForm = form(this.todoModel); // we create the form which is linked to the model

  titleField: Field<string> = this.todoForm.title;
  firstAssigned: Field<Assigned> = this.todoForm.assigned[0];
  firstAssignedName: Field<string> = firstAssigned.name;
}
Enter fullscreen mode Exit fullscreen mode

Binding the field to UI elements

We have now completed the data model definition, form creation, field tree navigation, and field instance retrieval.

It is now important to connect the UI element (input, textarea, or even a custom component) to the field so the user can interact with the control.

In Angular, the Signal Form provides a built-in directive, so you can see the source code here

This directive will have several responsibilities:

  • Two-way data binding to update the field's value (both from user interaction and programmatically).
  • Binding the field's business logic (validation rules, read-only status, etc.).
  • Relaying the various possible events on the control (dirty, touched, etc.).
  • Internally, injecting the NgControl token to get useful methods and ensure interoperability.

Here, we clearly see the intention to create a bridge between the HTML logic and Angular's forms, leveraging all the power they can provide.

Let's materialize this with some code :)

interface Assigned {
  name: string;
  firstname: string;
}

interface Todo {
  title: string;
  description: string;
  status: TodoStatus;
  assigned: Assigned[]
}


@Component({
  selector: 'app-form',
  templateUrl: './app-form.html',
  imports: [Control]
})
export class AppForm {
  todoModel = signal<Todo>({ 
    title: '',
    description: '', 
    status: 'not_begin',
    assigned: []
  }); // We create the model that will be the source of truth for the form and it's tree field

  todoForm = form(this.todoModel); // we create the form which is linked to the model
}
Enter fullscreen mode Exit fullscreen mode
<form novalidate>
  <input type="text" [control]="todoForm.title" />
  <input type="text" [control]="todoForm.description" />
  <select [control]="todoForm.status">
    <option [ngValue]="not_begin">Not Begined</option>
    <option [ngValue]="in_progress">In Progress</option>
    <option [ngValue]="finished">Finished</option>
  </select>
</form>
Enter fullscreen mode Exit fullscreen mode

Conclusion

This marks the end of the first article in a series of three. This article aimed to lay the foundations for Signal's forms.

We were able to understand how Signal's forms work by detailing their main components.

However, for now, our form contains no validation, and as a reminder, validation consists of the business logic of our field. This step will be detailed in the second article.

What to remember here is that the developer has complete control over the data model they expose. This data model will subsequently allow for the connection between the Field and the value.

The form function allows for the creation of a field tree. It is possible to navigate this tree using dot notation.

A Field instance allows you to retrieve its state by calling a function, which enables you, for example, to know its current value.

Top comments (0)