{"id":31238,"date":"2026-01-22T21:00:49","date_gmt":"2026-01-22T20:00:49","guid":{"rendered":"https:\/\/www.angulararchitects.io\/blog\/all-about-angulars-new-signal-forms\/"},"modified":"2026-04-20T20:44:41","modified_gmt":"2026-04-20T18:44:41","slug":"all-about-angulars-new-signal-forms","status":"publish","type":"post","link":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/","title":{"rendered":"Angular Signal Forms &#8211; Everything You Need to Know"},"content":{"rendered":"<div class=\"wp-post-series-box series-signal-forms-en wp-post-series-box--expandable\">\n\t\t\t<input id=\"collapsible-series-signal-forms-en6a1b2674a00bf\" class=\"wp-post-series-box__toggle_checkbox\" type=\"checkbox\">\n\t\n\t<label\n\t\tclass=\"wp-post-series-box__label\"\n\t\t\t\t\tfor=\"collapsible-series-signal-forms-en6a1b2674a00bf\"\n\t\t\ttabindex=\"0\"\n\t\t\t\t>\n\t\t<p class=\"wp-post-series-box__name wp-post-series-name\">\n\t\t\tThis is post 2 of 3 in the series <em>&ldquo;Signal Forms&rdquo;<\/em>\t\t<\/p>\n\t\t\t<\/label>\n\n\t\t\t<div class=\"wp-post-series-box__posts\">\n\t\t\t<ol>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/dynamic-forms-building-a-form-generator-with-signal-forms\/\">Dynamic Forms: Building a Form Generator with Signal Forms<\/a><\/li>\n\t\t\t\t\t\t\t\t\t<li><span class=\"wp-post-series-box__current\">Angular Signal Forms &#8211; Everything You Need to Know<\/span><\/li>\n\t\t\t\t\t\t\t\t\t<li><a href=\"https:\/\/www.angulararchitects.io\/en\/blog\/migrating-to-angular-signal-forms-interop-with-reactive-forms\/\">Migrating to Angular Signal Forms: Interop with Reactive Forms<\/a><\/li>\n\t\t\t\t\t\t\t<\/ol>\n\t\t<\/div>\n\t<\/div>\n<p><em>Last Update: April 2026<\/em><\/p>\n<p><a id=\"angular-signal-forms-building-reactive-forms-with-signals\"><\/a><\/p>\n<p>Angular Signals make component state predictable and reactive, but classic form APIs still introduce a separate model for values, validation, and submission. Angular Signal Forms close that gap by representing form state itself with Signals, so user input, validation, and UI feedback follow the same reactive model.<\/p>\n<p>This guide shows how Angular Signal Forms work in practice. Starting with a <code>FlightEdit<\/code> example, it covers the core API, <code>FieldTree<\/code>, schemas, submissions, advanced validation, nested forms, metadata, and custom controls so you can see where Signal Forms fit into modern Angular applications.<\/p>\n<p>We will use the following <code>FlightEdit<\/code> component as the running example:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/01\/solution2.png\" alt=\"Screenshot of the Angular &lt;code&gt;FlightEdit&lt;\/code&gt; screen with fields for flight data, aircraft details, prices, and validation-related form controls\" \/><\/p>\n<h2>Table of Contents<\/h2>\n<p><a id=\"table-of-contents\"><\/a><\/p>\n<ul>\n<li><a href=\"#how-to-create-a-signal-form-in-angular\">How to Create a Signal Form in Angular<\/a><\/li>\n<li><a href=\"#what-is-the-fieldtree-type-in-signal-forms\">What Is the FieldTree Type in Signal Forms?<\/a><\/li>\n<li><a href=\"#how-signal-form-schemas-work\">How Signal Form Schemas Work<\/a><\/li>\n<li><a href=\"#how-to-submit-signal-forms\">How to Submit Signal Forms<\/a><\/li>\n<li><a href=\"#how-validation-works-in-signal-forms\">How Validation Works in Signal Forms<\/a>: <a href=\"#how-to-write-custom-validators-for-angular-signal-forms\">Custom<\/a>, <a href=\"#how-conditional-validation-works-in-angular-signal-forms\">Conditional<\/a>, <a href=\"#how-cross-field-validation-works-in-angular-signal-forms\">Cross-Field<\/a>, <a href=\"#how-async-validation-works-in-angular-signal-forms\">Async<\/a><\/li>\n<li><a href=\"#how-to-work-with-nested-signal-forms-and-form-arrays\">How to Work with Nested Signal Forms and Form Arrays<\/a><\/li>\n<li><a href=\"#how-signal-form-metadata-works\">How Signal Form Metadata Works<\/a><\/li>\n<li><a href=\"#why-signal-forms-avoid-undefined-values\">Why Signal Forms Avoid Undefined Values<\/a><\/li>\n<li><a href=\"#how-to-build-custom-controls-for-signal-forms\">How to Build Custom Controls for Signal Forms<\/a><\/li>\n<li><a href=\"#angular-signal-forms-faq\">Angular Signal Forms FAQ<\/a><\/li>\n<\/ul>\n<h2>How to Create a Signal Form in Angular<\/h2>\n<p><a id=\"how-to-create-a-signal-form-in-angular\"><\/a><\/p>\n<p>Angular Signal Forms are part of <a href=\"mailto:code&gt;@angular\/forms\/signals&lt;\/code\">code>@angular\/forms\/signals<\/code<\/a>. Instead of maintaining a separate reactive form model, you bind a writable Signal to <code>form(...)<\/code> and get a <code>FieldTree<\/code> back that exposes values, validation state, metadata, and submission state as Signals.<\/p>\n<p>To get started, we create a first form for the flight itself. In later sections, we will extend it to include nested forms for the connected airplane and the prices.<\/p>\n<h3>How to Set Up a Signal Form Component in Angular<\/h3>\n<p><a id=\"how-to-set-up-a-signal-form-component-in-angular\"><\/a><\/p>\n<p>Our implementation receives the flight to be displayed from a service named <code>FlightDetailStore<\/code> that is similar to the store discussed in @sec:state-services. To avoid repetition, we won\u2019t go into its internals here. If you want to implement the concepts shown step by step, you can find a simplified version of this store for this chapter in the file <code>simple-flight-detail-store.ts<\/code>. In that case, also take a look at the <code>FlightClient<\/code>, which provides some additional methods used by this store.<\/p>\n<p>Since a store is responsible for data consistency, it only publishes read-only data. On the other hand, we want to modify the data using the form: A two-way binding should transfer the user input directly to the bound <code>flight<\/code> and vice versa. Hence, we need a local working copy that can be represented by a linked signal:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.ts\n\n[...]\n\nimport { linkedSignal } from &#039;@angular\/core&#039;;\nimport { form, minLength, required } from &#039;@angular\/forms\/signals&#039;;\n[...]\n\n@Component([...])\nexport class FlightEdit {\n  private readonly store = inject(FlightDetailStore);\n\n  protected readonly flight = linkedSignal(() =&gt;\n    normalizeFlight(this.store.flightValue()),\n  );\n\n  \/\/ Set up the Signal Form with validation rules\n  protected readonly flightForm = form(this.flight, (path) =&gt; {\n    required(path.from);\n    required(path.to);\n    required(path.date);\n\n    minLength(path.from, 3);\n  });\n\n  [...]\n}<\/code><\/pre>\n<p>The helper function <code>normalizeFlight<\/code> converts the flight date into the right format to be used with an <code>&lt;input type=&quot;datetime-local&quot;&gt;<\/code>. This is an ISO string without a timezone designator at the end, e.g., <code>2030-12-24T17:30:00.000<\/code>.<\/p>\n<pre><code class=\"language-typescript\">function normalizeFlight(flight: Flight): Flight {\n  const localDate = flight.date.substring(0, 16);\n  return {\n    ...flight,\n    date: localDate,\n  };\n}<\/code><\/pre>\n<p>The <code>form<\/code> function from Angular's Signal Forms package gets the linked signal representing the flight to edit.<\/p>\n<p>The second parameter that is passed to <code>form<\/code> is a schema with validation rules. Angular provides the <code>required<\/code> and <code>minLength<\/code> rules out of the box. Further simple validators that ship with Signal Forms include <code>maxLength<\/code>, <code>min<\/code>, <code>max<\/code>, and <code>pattern<\/code>. The latter checks against a regular expression.<\/p>\n<p>The <code>path<\/code> parameter of type <code>SchemaPathTree<\/code> is used to reference the individual properties we want to validate, e.g., <code>from<\/code> or <code>to<\/code>.<\/p>\n<p>As a result, <code>form<\/code> returns a <code>FieldTree<\/code> that allows binding the individual properties of the flight to form controls in the template. We'll discuss this important data structure in the next section.<\/p>\n<h3>What Is the FieldTree Type in Signal Forms?<\/h3>\n<p><a id=\"what-is-the-fieldtree-type-in-signal-forms\"><\/a><\/p>\n<p>You can think of a <code>FieldTree<\/code> as a deeply nested Signal. Each property in the data structure is represented by a Signal with form state, e.g., <code>value<\/code>, <code>dirty<\/code>, <code>invalid<\/code>. Because these properties are Signal-based, we can bind them to form controls.<\/p>\n<p>For instance, let's imagine the <code>flight<\/code> we pass to the function <code>form<\/code> looks like this:<\/p>\n<pre><code class=\"language-json\">{\n  id: 1,\n  from: &#039;Graz&#039;,\n  to: &#039;Hamburg&#039;,\n  date: &#039;2030-12-24T17:30&#039;,\n  delayed: false,\n  delay: 0,\n  aircraft: { \n    type: &#039;T0815&#039;, \n    registration: &#039;R4711&#039; \n  },\n  prices: [\n    { flightClass: &#039;economy&#039;, amount: 299 },\n    { flightClass: &#039;business&#039;, amount: 599 },\n  ]\n}<\/code><\/pre>\n<p>In this case, the <code>FieldTree<\/code> returned by the <code>form<\/code> provides Signals for each property:<\/p>\n<pre><code class=\"language-ts\">const date = this.flightForm.date().value(); \nconst isDateDirty = this.flightForm.date().dirty();\nconst isDateInvalid = this.flightForm.date().invalid();\nconst dateErrors = this.flightForm.date().errors();<\/code><\/pre>\n<p>Here, the <code>date<\/code> itself is a signal, and <code>value<\/code>, <code>dirty<\/code>, <code>invalid<\/code>, and <code>errors<\/code> are as well. The property <code>dirty<\/code> is <code>true<\/code> when the user modified the value, and <code>invalid<\/code> is <code>true<\/code> when there was a validation error.<\/p>\n<p>The <code>errors<\/code> property contains an array with all detected validation errors. For instance, the schema setup in the previous section requires <code>from<\/code> to be set. If this is not the case, this violation is reported by an object in the <code>errors<\/code> array. To get a better feeling for it, we'll display this array with the <code>JsonPipe<\/code>.<\/p>\n<p>As the <code>FieldTree<\/code> is nested, we can also access the properties at deeper levels, such as the airplane's type or the prices:<\/p>\n<pre><code class=\"language-ts\">const aircraftType = this.flightForm.aircraft.type().value();\nconst isAircraftTypeDirty = this.flightForm.aircraft.type().dirty();\n[...]\n\nconst firstPriceAmount = this.flightForm.prices[0].amount().value();\nconst isFirstPriceAmountDirty = this.flightForm.prices[0].amount().dirty();\n[...]<\/code><\/pre>\n<p>In our example, that means we can bind the properties of the flight itself, as well as those of the connected airplane and the connected prices. The next section is already binding some properties to the template.<\/p>\n<h3>How to Bind Angular Signal Forms to the Template<\/h3>\n<p><a id=\"how-to-bind-angular-signal-forms-to-the-template\"><\/a><\/p>\n<p>For binding parts of the Signal Form to the template, we need to import the <code>FormField<\/code> directive. For easily displaying validation errors during our first steps, we also import the <code>JsonPipe<\/code>:<\/p>\n<pre><code class=\"language-typescript\">\/\/ src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.ts\n\n[...]\n\nimport { JsonPipe } from &#039;@angular\/common&#039;;\n\nimport {\n  form,\n  FormField,\n  minLength,\n  required,\n} from &#039;@angular\/forms\/signals&#039;;\n\n@Component({\n  selector: &#039;app-flight-edit&#039;,\n  imports: [\n    [...]\n\n    \/\/ Import FormField directive\n    FormField,\n\n    \/\/ Add JsonPipe \n    JsonPipe,\n  ],\n  templateUrl: &#039;.\/flight-edit.html&#039;,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class FlightEdit {\n  [...]\n}<\/code><\/pre>\n<p>This directive can be used to bind individual fields of our <code>FieldTree<\/code> to form controls such as <code>input<\/code>, <code>select<\/code>, and <code>textarea<\/code>:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/ticketing\/feature-booking\/flight-edit\/flight-edit.html --&gt;\n\n&lt;fieldset&gt;\n  &lt;legend&gt;Flight&lt;\/legend&gt;\n\n  &lt;div class=&quot;form-group&quot;&gt;\n    &lt;label for=&quot;flight-id&quot;&gt;ID&lt;\/label&gt;\n    &lt;input\n      class=&quot;form-control&quot;\n      [formField]=&quot;flightForm.id&quot;\n      type=&quot;number&quot;\n      id=&quot;flight-id&quot;\n    \/&gt;\n  &lt;\/div&gt;\n\n  &lt;div class=&quot;form-group&quot;&gt;\n    &lt;label for=&quot;flight-from&quot;&gt;From&lt;\/label&gt;\n    &lt;input\n      class=&quot;form-control&quot;\n      [formField]=&quot;flightForm.from&quot;\n      id=&quot;flight-from&quot;\n    \/&gt;\n    @if (flightForm.from().invalid()) {\n      &lt;div&gt;{{ flightForm.from().errors() | json }}&lt;\/div&gt;\n    }\n  &lt;\/div&gt;\n  [\u2026]\n&lt;\/fieldset&gt;<\/code><\/pre>\n<p>Please note that when binding a number, you need to use an <code>&lt;input type=&quot;number&quot; ...&gt;<\/code> because Signal Forms also respects HTML semantics when ensuring type safety.<\/p>\n<p>The <code>errors<\/code> property is an array containing all validation errors detected for the field, which the component outputs for illustrative purposes. The following screenshot shows this array in a case where the <code>minLength<\/code> validator was triggered:<\/p>\n<p><!-- TODO: Update Screenshot --><\/p>\n<p><img decoding=\"async\" src=\"images\/min-length.png\" alt=\"Screenshot showing Signal Forms validation errors for the &lt;code&gt;from&lt;\/code&gt; field after the &lt;code&gt;minLength&lt;\/code&gt; validator was triggered\" \/>{width=50% }<\/p>\n<p>However, the <code>minLength<\/code> validator isn't triggered for empty fields. This common approach supports optional fields. However, if the field isn't optional, a <code>required<\/code> validator handles the empty field.<\/p>\n<p><div style=\"\nmargin: 8px 0;\npadding: 22px;\nborder: 1px solid #e5e7eb;\nborder-radius: 14px;\nbackground: #f8fafc;\n\">NOTE<\/p>\n<h3 style=\"margin-top:0\">Modern Angular<\/h3>\n<p>More about Signal Forms and modern Angular architecture can be found in my new eBook Modern Angular. It covers Signals, architecture, testing, AI assistants, and practical solutions for modern business applications.<\/p>\n<p><a href=\"https:\/\/www.angulararchitects.io\/en\/ebooks\/modern\/\"><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/01\/cover-klein.png\" width=\"400\" alt=\"Modern Angular - Signal-first, Architecture-first, Practice-first\" style=\"cursor:pointer !important\"><\/a><\/p>\n<p><a style=\"cursor:pointer !important\" href=\"https:\/\/www.angulararchitects.io\/en\/ebooks\/modern\/\">Learn more about the book \u2192<\/a>\n<\/div>\n<\/p>\n<h2>How Signal Form Schemas Work<\/h2>\n<p><a id=\"how-signal-form-schemas-work\"><\/a><\/p>\n<p>Besides validation rules, the schema passed to the <em>form<\/em> function also defines other aspects of the form's behavior. For instance, it could specify that changes are debounced or that some fields are read-only under certain conditions. In this section, I'll discuss these aspects in more detail.<\/p>\n<h3>How to Reuse Schemas in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-reuse-schemas-in-angular-signal-forms\"><\/a><\/p>\n<p>So far, the schema has been defined directly in the call to the <code>form<\/code> function. However, for larger schemas, this approach quickly becomes unwieldy. To make the individual components clearer, the schema can be refactored to a constant in a separate file. Since the schema defines general rules, it can be placed in the <code>data<\/code> folder.<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { required, minLength, schema } from &#039;@angular\/forms\/signals&#039;;\n\nimport { Flight } from &#039;.\/flight&#039;;\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  required(path.from);\n  required(path.to);\n  required(path.date);\n\n  minLength(path.from, 3);\n});<\/code><\/pre>\n<p>The call to the function <code>form<\/code> can then refer to it:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.ts\n\nimport { flightSchema } from &#039;..\/..\/data\/flight-schema&#039;;\n[...]\n\nprotected readonly flightForm = form(this.flight, flightSchema);\n[...]<\/code><\/pre>\n<p>Schemas can also reference each other. This is useful, for example, if a form adds some specific validation rules to a schema with general ones:<\/p>\n<pre><code class=\"language-ts\">import {\n  apply,\n  minLength,\n  required,\n  schema,\n} from &#039;@angular\/forms\/signals&#039;;\n\nimport { flightSchema } from &#039;..\/..\/data\/flight-schema&#039;;\n\nexport const flightFormSchema = schema&lt;Flight&gt;((path) =&gt; {\n  apply(path, flightSchema);\n  required(path.id);\n});<\/code><\/pre>\n<p>The shown <code>flightFormSchema<\/code> uses <code>apply<\/code> to include all rules defined in <code>flightSchema<\/code>. In addition, it requires the flight's ID to be set.<\/p>\n<p>Another case where schemas refer to other schemas is conditional validation rules. In the next section, we will discuss this.<\/p>\n<h3>How to Control Field Behavior in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-control-field-behavior-in-angular-signal-forms\"><\/a><\/p>\n<p>Schemas not only define validation rules but also further behavior. For instance, the schema can also specify under which circumstances the <code>formField<\/code> directive should disable a field:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { disabled } from &#039;@angular\/forms\/signals&#039;;\n\n[...]\nprotected readonly filterForm = form(this.filter, (schema) =&gt; {\n  [...]\n  disabled(path.delay, (ctx) =&gt; !ctx.valueOf(path.delayed));\n}\n[...]<\/code><\/pre>\n<p>Instead of a boolean, you can also return a reason for the disabling:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\nprotected readonly filterForm = form(this.filter, (schema) =&gt; {\n  [...]\n  disabled(path.delay, (ctx) =&gt;\n    ctx.valueOf(path.delayed) ? false : &#039;not delayed&#039;,\n  );\n}<\/code><\/pre>\n<p>All the reasons why a field is currently disabled are found in its <code>disabledReasons<\/code> Signal:<\/p>\n<pre><code class=\"language-html\">@for (reason of flightForm.delay().disabledReasons(); track $index) {\n  &lt;p&gt;Disabled because {{ reason.message }}&lt;\/p&gt;\n}<\/code><\/pre>\n<p>The functions <code>hidden<\/code> and <code>readonly<\/code> can be used to hide or make fields read-only in the same way. However, they always return boolean values and don't support reasons.<\/p>\n<p>While Signal Forms automatically disables writing to bound fields, e.g., input elements, marked as read-only, hiding fields is left to the application. This is because, typically, hiding a field also requires hiding its label and perhaps other UI elements. Hence, hidden is only a hint that your template can use:<\/p>\n<pre><code class=\"language-html\">@if (!flightForm.delay().hidden()) {\n  &lt;div class=&quot;form-group&quot;&gt;\n    &lt;label for=&quot;flight-delay&quot;&gt;Delay (min)&lt;\/label&gt;\n    &lt;input\n      class=&quot;form-control&quot;\n      [formField]=&quot;flightForm.delay&quot;\n      type=&quot;number&quot;\n      id=&quot;flight-delay&quot;\n    \/&gt;\n  &lt;\/div&gt;\n}<\/code><\/pre>\n<h3>How to Debounce Angular Signal Forms<\/h3>\n<p><a id=\"how-to-debounce-angular-signal-forms\"><\/a><\/p>\n<p>Especially in reactive UIs with search filters, debouncing is essential: instead of firing an HTTP request on every keystroke, we trigger it only after the user pauses typing. In our example, this behavior appears in the flight search form.<\/p>\n<p>As already teased in the previous chapter, the form's debouncing behavior is specified in its schema. For this, we added the <code>debounce<\/code> function in the <code>FlightSearch<\/code> component:<\/p>\n<pre><code class=\"language-ts\">\/\/ ...\/feature-booking\/flight-search\/flight-search.ts\n\n[...]\n\nimport { debounce, form, minLength, required } from &#039;@angular\/forms\/signals&#039;;\n\n[...]\n\nprotected readonly filterForm = form(this.filter, (path) =&gt; {\n  debounce(path, 300);\n\n  required(path.from);\n  minLength(path.from, 3);\n});<\/code><\/pre>\n<p>In cases where you want to debounce until the user left the input field, you can specify <code>blur<\/code> instead of a debounce time:<\/p>\n<pre><code class=\"language-ts\">protected readonly filterForm = form(this.filter, (path) =&gt; {\n  debounce(path, &#039;blur&#039;);\n});<\/code><\/pre>\n<p>In most cases, just calling <code>debounce<\/code> with the debounce time in milliseconds is exactly what you need. However, if you want to control the debounce time using code, you can also pass in a custom debouncer function:<\/p>\n<pre><code class=\"language-ts\">protected readonly filterForm = form(this.filter, (path) =&gt; {\n  debounce(path, (ctx, _abortSignal) =&gt; {\n    return new Promise((resolve) =&gt; {\n      setTimeout(resolve, 300);\n    });\n  });\n\n  required(path.from);\n  minLength(path.from, 3);\n});<\/code><\/pre>\n<p>Now, the returned Promise controls the debouncing. When it resolves, Angular continues processing the changes.<\/p>\n<h3>How to Validate Angular Signal Forms with Zod and Standard Schema<\/h3>\n<p><a id=\"how-to-validate-angular-signal-forms-with-zod-and-standard-schema\"><\/a><\/p>\n<p>Often, there is already something, such as a <a href=\"https:\/\/zod.dev\">Zod<\/a> schema, that describes rules for valid objects. Perhaps you already have them for server-side code, or you generate them from a JSON Schema or an Open API document. Fortunately, you can directly validate against such schemas.<\/p>\n<p>Let's imagine, Zod was already installed in our project (<code>npm i zod<\/code>) and we have this Zod schema for flights:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-zod-schema.ts\n\nimport { z } from &#039;zod&#039;;\n\nexport const FlightZodSchema = z.object({\n  id: z.number().int(),\n  from: z.string().min(3).max(20),\n  to: z.string().min(3).max(20),\n  date: z.string(),\n  delayed: z.boolean(),\n})<\/code><\/pre>\n<p>Now, our Signal Form schema use <code>validateStandardSchema<\/code> to reference the Zod schema and use it to validate the form:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { validateStandardSchema, schema } from &#039;@angular\/forms\/signals&#039;;\nimport { FlightZodSchema } from &#039;.\/flight-zod-schema&#039;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  validateStandardSchema(path, FlightZodSchema);\n  \/\/ ... other validation rules\n});<\/code><\/pre>\n<p>This not only works with Zod but also with all other schema libraries that follow the Standard Schema definition such as <a href=\"https:\/\/valibot.dev\">Valibot<\/a>.<\/p>\n<p>Combined with the <code>submit<\/code> function discussed above, this is a quick and effective way to establish a client-side validation.<\/p>\n<h2>How to Submit Signal Forms<\/h2>\n<p><a id=\"how-to-submit-signal-forms\"><\/a><\/p>\n<p>The perhaps biggest improvement in Signals Forms is about how we submit forms: We can now define the logic for submitting a form when calling the <code>form<\/code> function. After binding the resulting <code>FieldTree<\/code> to the <code>form<\/code> tag, all we need is a regular submit button. This section explains these new capabilities in more detail.<\/p>\n<h3>How Form Submission Works in Angular Signal Forms<\/h3>\n<p><a id=\"how-form-submission-works-in-angular-signal-forms\"><\/a><\/p>\n<p>For defining our submission logic, we use the new <code>submission<\/code> node in the <code>options<\/code> object that can be passed to the <code>form<\/code> helper:<\/p>\n<pre><code class=\"language-ts\">\/\/ ...\/ticketing\/feature-booking\/flight-edit\/flight-edit.ts\n\n@Component({\n  selector: &#039;app-flight-edit&#039;,\n  imports: [\n    [...]\n    FormRoot,\n  ],\n  [...]\n})\nexport class FlightEdit {\n\n  [...]\n\n  protected readonly flightForm = form(this.flight, flightSchema, {\n    submission: {\n      action: async (form) =&gt; this.save(form),\n      ignoreValidators: &#039;none&#039;,\n      onInvalid: (form) =&gt; this.reportValidationError(form),\n    },\n  });\n\n  [...]\n}<\/code><\/pre>\n<p>The action property points to the function to be executed when the user submits the form. By default, this method is not executed when there is any failing or pending validator. Pending validators are asynchronous validators that did not deliver a result yet. Using the <code>ignoreValidators<\/code> property, we can, however, change this behavior. It can have one of the following values:<\/p>\n<ul>\n<li><code>none<\/code>: Validators are not ignored and the form cannot be submitted when any validator is failing or pending. This is the default behavior.<\/li>\n<li><code>pending<\/code>: Pending validators do not prevent submitting the form.<\/li>\n<li><code>all<\/code>: All Validators are ignored. Hence, we can submit the form even when validators are failing or pending.<\/li>\n<\/ul>\n<p>The <code>onInvalid<\/code> handler is executed when a submission is prevented by a failing validator.<\/p>\n<p>The method registered with <code>action<\/code> can return validation error the application gets aware of when sending the data to the backend:<\/p>\n<pre><code class=\"language-ts\">protected async save(form: FieldTree&lt;Flight&gt;) {\n  try {\n    await this.store.saveFlight(form().value());\n    return null;\n  } catch (error) {\n    return {\n      kind: &#039;processing_error&#039;,\n      error: extractError(error),\n    };\n  }\n}<\/code><\/pre>\n<p>These validation errors are placed in the object graph describing the Signal Form. Hence, the forms error property will provide the error message:<\/p>\n<pre><code class=\"language-ts\">&lt;p&gt;\n  {{ flightForm().errorSummary() | json }}\n&lt;\/p&gt;<\/code><\/pre>\n<p>This interplay between validation errors received on the client side and validation errors we got during the submission is a welcoming new feature of Signal Forms. With the previous form implementations, such a behavior was difficult to achieve.<\/p>\n<p>In our example the <code>onInvalid<\/code> handler is calling the <code>reportValidationError<\/code> that is displaying a snack bar and focussing the first input with a validation error:<\/p>\n<pre><code class=\"language-ts\">private reportValidationError(form: FieldTree&lt;Flight&gt;): void {\n  this.snackBar.open(&#039;Please correct the validation errors&#039;, &#039;OK&#039;);\n  this.focusInvalid(form);\n}\n\nprivate focusInvalid(form: FieldTree&lt;Flight&gt;) {\n  const errors = form().errorSummary();\n  if (errors.length &gt; 0) {\n    errors[0].fieldTree().focusBoundControl();\n  }\n}<\/code><\/pre>\n<h3>How to Submit Angular Signal Forms in the Template<\/h3>\n<p><a id=\"how-to-submit-angular-signal-forms-in-the-template\"><\/a><\/p>\n<p>To submit the form, we just need to connect the form element to our Signal Form. For this, we use the newly introduced <code>formRoot<\/code> directive:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/ticketing\/feature-booking\/flight-edit\/flight-edit.html --&gt;\n\n&lt;h1&gt;Flight Edit&lt;\/h1&gt;\n\n&lt;form [formRoot]=&quot;flightForm&quot;&gt;\n\n  [...]\n\n  &lt;div&gt;\n    &lt;button&gt;Save&lt;\/button&gt;\n  &lt;\/div&gt;\n&lt;\/form&gt;<\/code><\/pre>\n<p>This new directive takes care of several tasks at once:<\/p>\n<ul>\n<li>Disabling at default submission behavior, as we don't want to post back the form's content to the server as in traditional server-side rendered applications.<\/li>\n<li>Disabling the browser's built-in form validation, as Angular is taking care of this and we don't want to duplicate some of the validation messages.<\/li>\n<li>Connecting the submission action to the form.<\/li>\n<\/ul>\n<p>As <code>formRoot<\/code> directly registeres the defined <code>action<\/code> with the form's <code>submit<\/code> event, we just need to add an ordinary button with the type <code>submit<\/code> to the form. As <code>submit<\/code> is the default type, we don't even need to add <code>type=&quot;submit&quot;<\/code> as shown in the example above. But also when submitting the form by pressing enter, the <code>action<\/code> will be triggered.<\/p>\n<h3>How to Add Additional Submit Actions in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-add-additional-submit-actions-in-angular-signal-forms\"><\/a><\/p>\n<p>If you have further submission actions, e.g., for handing in changes for approval, you can always add further buttons with <code>type=&quot;button&quot;<\/code> and bind them to custom click handlers. In such cases, the <code>submit<\/code> function provided by Signal Forms helps to only execute the submission logic when the form is valid:<\/p>\n<pre><code class=\"language-ts\">protected async requestApproval(): Promise&lt;void&gt; {\n  await submit(this.flightForm, {\n    action: async (form) =&gt; {\n      await this.store.requestApproval(form().value());\n    },\n    ignoreValidators: &#039;none&#039;,\n    onInvalid: (form) =&gt; this.reportValidationError(form),\n  });\n}<\/code><\/pre>\n<h2>How Validation Works in Signal Forms<\/h2>\n<p><a id=\"how-validation-works-in-signal-forms\"><\/a><\/p>\n<p>Custom validators allow you to implement validation logic that goes beyond the built-in validators, such as <code>required<\/code> and <code>minLength<\/code>. They can check against business rules and compare values.<\/p>\n<h3>How to Write Custom Validators for Angular Signal Forms<\/h3>\n<p><a id=\"how-to-write-custom-validators-for-angular-signal-forms\"><\/a><\/p>\n<p>To define a custom validator, the <code>validate<\/code> function is used. It accepts the path to be validated and a lambda expression that performs the validation:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { validate } from &#039;@angular\/forms\/signals&#039;;\n\n[...]\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  required(path.from);\n  required(path.to);\n  required(path.date);\n  minLength(path.from, 3);\n\n  const allowed = [&#039;Graz&#039;, &#039;Hamburg&#039;, &#039;Z\u00fcrich&#039;];\n  validate(path.from, (ctx) =&gt; {\n    const value = ctx.value();\n    if (allowed.includes(value)) {\n      return null;\n    }\n\n    return {\n      kind: &#039;city&#039;,\n      value,\n      allowed,\n    };\n  });\n});<\/code><\/pre>\n<p>This example defines a custom rule that checks the field <code>from<\/code> against a list of allowed airports. In case of an error, this validator returns a <code>ValidationError<\/code>, which identifies the validation error via a string property called <code>kind<\/code>. The validator can provide more detailed error information using additional properties it can freely specify.<\/p>\n<p>If a validator detects multiple errors, it can also return an array of <code>ValidationError<\/code> objects. If there are no errors, it returns the value <code>null<\/code> \u2013 true to the motto: no complaints are praise enough.<\/p>\n<h3>How to Refactor Signal Form Validators into Reusable Functions<\/h3>\n<p><a id=\"how-to-refactor-signal-form-validators-into-reusable-functions\"><\/a><\/p>\n<p>To improve clarity and reuse, it is advisable to refactor custom validators to separate functions. They must at least accept the property for validation. This property is represented by the type <code>SchemaPathTree&lt;T&gt;<\/code>:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-validators.ts\n\nimport { SchemaPathTree, validate } from &#039;@angular\/forms\/signals&#039;;\n\nexport function validateCity(path: SchemaPathTree&lt;string&gt;, allowed: string[]) {\n  validate(path, (ctx) =&gt; {\n    const value = ctx.value();\n    if (allowed.includes(value)) {\n      return null;\n    }\n\n    return {\n      kind: &#039;city&#039;,\n      value,\n      allowed,\n    };\n  });\n}<\/code><\/pre>\n<p>This validator can check a string property passed via the <code>path<\/code> parameter against the list of allowed cities passed to the <code>allowed<\/code> parameter. To use the validator, the application calls it in the schema:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { validateCity } from &#039;.\/flight-validators&#039;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  [...]\n  validateCity(path.from, [&#039;Graz&#039;, &#039;Hamburg&#039;, &#039;Z\u00fcrich&#039;]);\n});<\/code><\/pre>\n<h3>How to Show Validation Errors in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-show-validation-errors-in-angular-signal-forms\"><\/a><\/p>\n<p>So far, we've only returned the <code>errors<\/code> object to describe the detected validation errors. However, the validators provided by Angular also give us the option to specify an error message for the user:<\/p>\n<pre><code class=\"language-ts\">required(path.from, { message: &#039;Please enter a value!&#039; });<\/code><\/pre>\n<p>The validator places this error message in the <code>message<\/code> property of the respective <code>ValidationError<\/code> object. This means the application only needs to output all <code>message<\/code> properties it finds in the <code>errors<\/code> array.<\/p>\n<p>In our example, the <code>ValidationErrorsPane<\/code> component handles this task. If a validation error does not have a <code>message<\/code> property, a standard error message determined by the helper function <code>toMessage<\/code> is used:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/shared\/ui-forms\/validation-errors\/validation-errors-pane.ts\n\nimport { Component, computed, input } from &#039;@angular\/core&#039;;\nimport { MinValidationError, ValidationError } from &#039;@angular\/forms\/signals&#039;;\n\n@Component({\n  selector: &#039;app-validation-errors-pane&#039;,\n  imports: [],\n  templateUrl: &#039;.\/validation-errors-pane.html&#039;,\n})\nexport class ValidationErrorsPane {\n  readonly errors = input.required&lt;ValidationError.WithField[]&gt;();\n  readonly showFieldNames = input(false);\n\n  protected readonly errorMessages = computed(() =&gt;\n    toErrorMessages(this.errors(), this.showFieldNames()),\n  );\n}\n\nfunction toErrorMessages(\n  errors: ValidationError.WithField[],\n  showFieldNames: boolean,\n): string[] {\n  return errors.map((error) =&gt; {\n    const prefix = showFieldNames ? toFieldName(error) + &#039;: &#039; : &#039;&#039;;\n\n    const message = error.message ?? toMessage(error);\n    return prefix + message;\n  });\n}\n\nfunction toFieldName(error: ValidationError.WithField) {\n  return error.fieldTree().name().split(&#039;.&#039;).at(-1);\n}\n\nfunction toMessage(error: ValidationError): string {\n  switch (error.kind) {\n    case &#039;required&#039;:\n      return &#039;Enter a value!&#039;;\n    case &#039;roundtrip&#039;:\n    case &#039;roundtrip_tree&#039;:\n      return &#039;Roundtrips are not supported!&#039;;\n    case &#039;min&#039;:\n      return <code>Minimum amount: ${(error as MinValidationError).min}<\/code>;\n    default:\n      return error.kind ?? &#039;Validation Error&#039;;\n  }\n}<\/code><\/pre>\n<p>The template displays the error messages obtained in this way:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/shared\/ui-forms\/validation-errors\/validation-errors-pane.html --&gt;\n\n@if (errorMessages().length &gt; 0) {\n  &lt;div class=&quot;validation-errors&quot;&gt;\n    @for (message of errorMessages(); track message) {\n      &lt;div&gt;{{ message }}&lt;\/div&gt;\n    }\n  &lt;\/div&gt;\n}<\/code><\/pre>\n<p>When using the <code>ValidationErrorsPane<\/code>, don't forget to import it in the consuming component:<\/p>\n<pre><code class=\"language-typescript\">\/\/ src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.ts\n\n[...]\n\nimport { ValidationErrorsPane } \n  from &#039;..\/..\/shared\/ui-forms\/validation-errors\/validation-errors-pane&#039;;\n[...]\n\n@Component({\n  selector: &#039;app-flight-edit&#039;,\n  imports: [[...], ValidationErrorsPane],\n  [...]\n})\nexport class FlightEdit {\n  [...]\n}<\/code><\/pre>\n<p>Then, in the template, call it for each field and pass the <code>errors<\/code> array:<\/p>\n<pre><code class=\"language-html\">&lt;!-- src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.html --&gt;\n&lt;div class=&quot;form-group&quot;&gt;\n  &lt;label for=&quot;flight-from&quot;&gt;From&lt;\/label&gt;\n  &lt;input [formField]=&quot;flightForm.from&quot; id=&quot;flight-from&quot; \/&gt;\n  &lt;app-validation-errors-pane [errors]=&quot;flightForm.from().errors()&quot; \/&gt;\n&lt;\/div&gt;<\/code><\/pre>\n<h3>How Conditional Validation Works in Angular Signal Forms<\/h3>\n<p><a id=\"how-conditional-validation-works-in-angular-signal-forms\"><\/a><\/p>\n<p>Some validation rules only apply under specific circumstances. In the application shown, <code>delay<\/code> needs to be validated only when <code>delayed<\/code> is <code>true<\/code>. This simple case is representative of more complex cases in which the value of one field affects the validation of other fields.<\/p>\n<p>Such situations can be handled with the <code>applyWhenValue<\/code> function. It accepts a path to validate, a predicate \u2014 typically a lambda expression \u2014, and a schema. If the predicate returns <code>true<\/code>, the passed schema with all its validation rules is applied:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { applyWhenValue, required, min, schema } from &#039;@angular\/forms\/signals&#039;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  \/\/ ...\n  applyWhenValue(path, (flight) =&gt; flight.delayed, delayedFlight);\n});\n\nexport const delayedFlight = schema&lt;Flight&gt;((path) =&gt; {\n  required(path.delay);\n  min(path.delay, 15);\n});<\/code><\/pre>\n<p>The schema contains the conditional validation rules. There are a few alternatives to <code>applyWhenValue<\/code>. One of them is <code>applyWhen<\/code>, whose predicate receives not just the value but the current context, which provides access to the entire field state:<\/p>\n<pre><code class=\"language-ts\">applyWhen(path, (ctx) =&gt; ctx.valueOf(path.delayed), delayedFlight);<\/code><\/pre>\n<p>Besides <code>valueOf<\/code>, the context also provides a function <code>stateOf<\/code> that returns the entire field state of a given path. This is useful if the predicate needs to check properties such as <code>dirty<\/code>.<\/p>\n<p>Another option for conditional validations is the <code>when<\/code> property provided by the <code>required<\/code> validator:<\/p>\n<pre><code class=\"language-ts\">required(path.delay, {\n  when: (ctx) =&gt; ctx.valueOf(path.delayed),\n});<\/code><\/pre>\n<h3>How Cross-Field Validation Works in Angular Signal Forms<\/h3>\n<p><a id=\"how-cross-field-validation-works-in-angular-signal-forms\"><\/a><\/p>\n<p>Sometimes we need to asses multiple fields to perform a validation. One option for achieving this is a validator provided for a common parent. For example, if a validator needs to ensure that <code>from<\/code> and <code>to<\/code> have different values, the application can implement it as a validator for the entire <code>flight<\/code>:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-validators.ts\n\nimport { SchemaPathTree, validate } from &#039;@angular\/forms\/signals&#039;;\nimport { Flight } from &#039;.\/flight&#039;;\n\nexport function validateRoundTrip(path: SchemaPathTree&lt;Flight&gt;) {\n  validate(path, (ctx) =&gt; {\n    const from = ctx.fieldTree.from().value();\n    const to = ctx.fieldTree.to().value();\n\n    \/\/ Alternative:\n    \/\/ const from = ctx.valueOf(path.from);\n    \/\/ const to = ctx.valueOf(path.to);\n\n    if (from === to) {\n      return {\n        kind: &#039;roundtrip&#039;,\n        from,\n        to,\n      };\n    }\n    return null;\n  });\n}<\/code><\/pre>\n<p>As usual, this validator must be included in the schema:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\n[...]\nimport { validateRoundTrip } from &#039;.\/flight-validators&#039;;\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  \/\/ ...\n  validateRoundTrip(path);\n});<\/code><\/pre>\n<p>Error messages returned by a validator are always associated with the validated level in the <code>FieldTree<\/code>. In the shown example, this is the <code>flight<\/code> itself, not <code>from<\/code> or <code>to<\/code>. Therefore, the template needs to access the <code>flightForm<\/code>'s <code>errors<\/code> array to display this error message:<\/p>\n<pre><code class=\"language-html\">&lt;!-- src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.html --&gt;\n\n&lt;app-validation-errors-pane [errors]=&quot;flightForm().errors()&quot; \/&gt;\n&lt;form&gt;[\u2026]&lt;\/form&gt;<\/code><\/pre>\n<p>In the screenshot shown initially, this is the first error message the component displays above the form. Validation errors from lower levels do not appear in <code>errors<\/code>. To retrieve these, the application can access the <code>errors<\/code> array on the respective level or a parent's <code>errorSummary<\/code>:<\/p>\n<pre><code class=\"language-html\">&lt;app-validation-errors-pane \n  [errors]=&quot;flightForm().errorSummary()&quot; \n  [showFieldNames]=&quot;true&quot;\n  \/&gt;\n&lt;form&gt;[\u2026]&lt;\/form&gt;<\/code><\/pre>\n<p>Since this will reveal error messages for different levels and fields, it makes sense to output the name of the affected field for each error.<\/p>\n<h3>How to Access Sibling Fields in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-access-sibling-fields-in-angular-signal-forms\"><\/a><\/p>\n<p>An alternative implementation of the round-trip validator in the previous section is to define a validator for <code>from<\/code> that also has access to its sibling field <code>to<\/code>. This is done using the <code>valueOf<\/code> function provided by the validation context:<\/p>\n<pre><code class=\"language-typescript\">export function validateRoundTrip2(path: SchemaPathTree&lt;Flight&gt;) {\n  \/\/ Now, we are validating the &#039;from&#039; field only\n  validate(path.from, (ctx) =&gt; {\n    const from = ctx.value();\n    const to = ctx.valueOf(path.to);\n\n    if (from === to) {\n      return {\n        kind: &#039;roundtrip&#039;,\n        from,\n        to,\n      };\n    }\n    return null;\n  });\n}<\/code><\/pre>\n<p>In this case, the error message appears in the <code>from<\/code> field's <code>errors<\/code> array:<\/p>\n<pre><code class=\"language-html\">&lt;app-validation-errors-pane [errors]=&quot;flightForm.from().errors()&quot; \/&gt;<\/code><\/pre>\n<h3>How Tree Validators Work in Angular Signal Forms<\/h3>\n<p><a id=\"how-tree-validators-work-in-angular-signal-forms\"><\/a><\/p>\n<p>Tree validators are special multi-field validators that can define error messages for all levels of a field tree. To do so, they store the affected field in the <code>ValidationError<\/code> object:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-validators.ts\n\nimport { SchemaPathTree, validateTree } from &#039;@angular\/forms\/signals&#039;;\nimport { Flight } from &#039;.\/flight&#039;;\n\nexport function validateRoundTripTree(path: SchemaPathTree&lt;Flight&gt;) {\n  validateTree(path, (ctx) =&gt; {\n    const from = ctx.fieldTree.from().value();\n    const to = ctx.fieldTree.to().value();\n\n    if (from === to) {\n      return {\n        kind: &#039;roundtrip_tree&#039;,\n        field: ctx.fieldTree.from,\n        from,\n        to,\n      };\n    }\n    return null;\n  });\n}<\/code><\/pre>\n<p>After registering in the schema, the error message now appears at the field level:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\n[...]\nimport { validateRoundTripTree } from &#039;.\/flight-validators&#039;;\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  \/\/ ...\n  validateRoundTripTree(path);\n});<\/code><\/pre>\n<p>Since each validator can return multiple error messages in the form of an array, a tree validator can also return error messages for different fields. This makes it ideal for complex rules. Simple rules, however, can also be implemented as conventional validators that have access to neighboring fields as shown before.<\/p>\n<h3>How Async Validation Works in Angular Signal Forms<\/h3>\n<p><a id=\"how-async-validation-works-in-angular-signal-forms\"><\/a><\/p>\n<p>Asynchronous validators do not deliver validation results instantly; they determine them asynchronously. Typically, this is done via an HTTP call. The <code>validateAsync<\/code> function provided by Signal Forms enables this approach. It defines four mappings:<\/p>\n<ul>\n<li><code>params<\/code> maps the form state to parameters<\/li>\n<li><code>factory<\/code> creates a resource with these parameters<\/li>\n<li><code>onSuccess<\/code> maps the result determined by the resource to a <code>ValidationError<\/code> or a <code>ValidationError<\/code> array<\/li>\n<li><code>onError<\/code> maps a possible error returned by the resource to a <code>ValidationError<\/code> or a <code>ValidationError<\/code> array<\/li>\n<\/ul>\n<p>The following example demonstrates this using an <code>rxResource<\/code> that calls the <code>rxValidateAirport<\/code> function to simulate an HTTP call for validating an airport:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-validators.ts\n\nimport { rxResource } from &#039;@angular\/core\/rxjs-interop&#039;;\nimport { SchemaPathTree, validateAsync } from &#039;@angular\/forms\/signals&#039;;\nimport { delay, map, Observable, of } from &#039;rxjs&#039;;\n\nexport function validateCityAsync(path: SchemaPathTree&lt;string&gt;) {\n  validateAsync(path, {\n    params: (ctx) =&gt; ({\n      value: ctx.value(),\n    }),\n    factory: (params) =&gt; {\n      return rxResource({\n        params,\n        stream: (p) =&gt; {\n          return rxValidateAirport(p.params.value);\n        },\n      });\n    },\n    onSuccess: (result: boolean, _ctx) =&gt; {\n      if (!result) {\n        return {\n          kind: &#039;airport_not_found_http&#039;,\n        };\n      }\n      return null;\n    },\n    onError: (error, _ctx) =&gt; {\n      console.error(&#039;api error validating city&#039;, error);\n      return {\n        kind: &#039;api-failed&#039;,\n      };\n    },\n  });\n}\n\n\/\/ Simulates a server-side validation\nfunction rxValidateAirport(airport: string): Observable&lt;boolean&gt; {\n  const allowed = [&#039;Graz&#039;, &#039;Hamburg&#039;, &#039;Z\u00fcrich&#039;];\n  return of(null).pipe(\n    delay(2000),\n    map(() =&gt; allowed.includes(airport)),\n  );\n}<\/code><\/pre>\n<p>This validator must also be added to the schema:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { validateCityAsync } from &#039;.\/flight-validators&#039;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  [...]\n  validateCityAsync(path.from);\n});<\/code><\/pre>\n<p>As long as at least one synchronous validator fails, Angular doesn't execute asynchronous validators. This prevents unnecessary server calls. While Signal Forms waits for the result of an asynchronous validator, the <code>pending<\/code> property of the affected field is <code>true<\/code>:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/ticketing\/feature-booking\/flight-edit\/flight-form\/flight-form.html --&gt;\n\n@if (flightForm.from().pending()) {\n  &lt;div&gt;Waiting for Async Validation Result...&lt;\/div&gt;\n}<\/code><\/pre>\n<h3>How HTTP Validators Work in Angular Signal Forms<\/h3>\n<p><a id=\"how-http-validators-work-in-angular-signal-forms\"><\/a><\/p>\n<p>Most asynchronous validators initiate HTTP requests. Therefore, <code>validateHttp<\/code> provides a simplified version that directly returns a request for an <code>HttpResource<\/code>:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-validators.ts\n\nimport { SchemaPathTree, validateHttp, metadata } from &#039;@angular\/forms\/signals&#039;;\nimport { Flight } from &#039;.\/flight&#039;;\n\nexport function validateCityHttp(path: SchemaPathTree&lt;string&gt;) {\n  validateHttp(path, {\n    request: (ctx) =&gt; ({\n      url: &#039;https:\/\/demo.angulararchitects.io\/api\/flight&#039;,\n      params: {\n        from: ctx.value(),\n      },\n    }),\n    onSuccess: (result: Flight[], _ctx) =&gt; {\n      if (result.length === 0) {\n        return {\n          kind: &#039;airport_not_found_http&#039;,\n        };\n      }\n      return null;\n    },\n    onError: (error, _ctx) =&gt; {\n      console.error(&#039;api error validating city&#039;, error);\n      return {\n        kind: &#039;api-failed&#039;,\n      };\n    },\n  });\n}<\/code><\/pre>\n<p>Like before, <code>onSuccess<\/code> maps the result of the resource to <code>ValidationError<\/code> objects and <code>onError<\/code> projects errors provided by the resource to this type. Here, too, you should remember to anchor the validator in the schema:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { validateCityHttp } from &#039;.\/flight-validators&#039;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  [...]\n  validateCityHttp(path.from);\n});<\/code><\/pre>\n<h2>How to Work with Nested Signal Forms and Form Arrays<\/h2>\n<p><a id=\"how-to-work-with-nested-signal-forms-and-form-arrays\"><\/a><\/p>\n<p>Real-world applications often require forms that go beyond simple flat structures. Signal Forms support complex, nested data models with objects and arrays. This section demonstrates how to work with form groups (nested objects) and form arrays (repeating groups) and how to break down large forms into smaller, more manageable sub-components.<\/p>\n<h3>Nested Form Groups in Angular Signal Forms<\/h3>\n<p><a id=\"nested-form-groups-in-angular-signal-forms\"><\/a><\/p>\n<p>So far, we've only bound a flat <code>flight<\/code> object to a form. However, Signal Forms also support more complex structures. To illustrate this, let's write validation rules for the <code>aircraft<\/code> object connected to a flight:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/aircraft-schema.ts\n\nimport { required, schema } from &#039;@angular\/forms\/signals&#039;;\nimport { Aircraft } from &#039;.\/aircraft&#039;;\n\nexport const aircraftSchema = schema&lt;Aircraft&gt;((path) =&gt; {\n  required(path.registration);\n  required(path.type);\n});<\/code><\/pre>\n<p>The flight schema uses the aircraft schema:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { apply } from &#039;@angular\/forms\/signals&#039;;\nimport { aircraftSchema } from &#039;.\/aircraft-schema&#039;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  [...]\n  apply(path.aircraft, aircraftSchema);\n});<\/code><\/pre>\n<p>The aircraft can be directly accessed via the flight. To avoid long chains like <code>flightForm.aircraft.registration().errors()<\/code>, I create an alias for each additional level using <a href=\"mailto:code&gt;@let&lt;\/code\">code>@let<\/code<\/a>:<\/p>\n<pre><code class=\"language-html\">&lt;!-- src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.html --&gt;\n\n@let aircraftForm = aircraft();\n\n&lt;fieldset&gt;\n  &lt;legend&gt;Aircraft&lt;\/legend&gt;\n\n  &lt;div class=&quot;form-group&quot;&gt;\n    &lt;label for=&quot;type&quot;&gt;Type:&lt;\/label&gt;\n    &lt;input id=&quot;type&quot; class=&quot;form-control&quot; [formField]=&quot;aircraftForm.type&quot; \/&gt;\n    &lt;app-validation-errors-pane [errors]=&quot;aircraftForm.type().errors()&quot; \/&gt;\n  &lt;\/div&gt;\n  &lt;div class=&quot;form-group&quot;&gt;\n    &lt;label for=&quot;registration&quot;&gt;Registration:&lt;\/label&gt;\n    &lt;input\n      id=&quot;registration&quot;\n      class=&quot;form-control&quot;\n      [formField]=&quot;aircraftForm.registration&quot;\n    \/&gt;\n    &lt;app-validation-errors-pane\n      [errors]=&quot;aircraftForm.registration().errors()&quot;\n    \/&gt;\n  &lt;\/div&gt;\n&lt;\/fieldset&gt;<\/code><\/pre>\n<h3>Form Arrays in Angular Signal Forms<\/h3>\n<p><a id=\"form-arrays-in-angular-signal-forms\"><\/a><\/p>\n<p>The repeating group with the <code>prices<\/code> is designed similarly to the aircraft form. First, you need to set up a schema:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/price-schema.ts\n\nimport { min, required, schema } from &#039;@angular\/forms\/signals&#039;;\nimport { Price } from &#039;.\/price&#039;;\n\nexport const initialPrice: Price = {\n  flightClass: &#039;&#039;,\n  amount: 0,\n};\n\nexport const priceSchema = schema&lt;Price&gt;((path) =&gt; {\n  required(path.flightClass);\n  required(path.amount);\n  min(path.amount, 0);\n});<\/code><\/pre>\n<p>Unlike before, the flight schema must set up the pricing scheme for each individual price. Therefore, the <code>applyEach<\/code> function is used here instead of <code>apply<\/code>:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { applyEach, schema } from &#039;@angular\/forms\/signals&#039;;\nimport { priceSchema } from &#039;.\/price-schema&#039;;\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  \/\/ ...\n  applyEach(path.prices, priceSchema);\n});<\/code><\/pre>\n<p>In the template, the prices must now be iterated, and corresponding fields must be presented for each price:<\/p>\n<pre><code class=\"language-html\">&lt;!-- src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.html --&gt;\n\n@let pricesForm = prices();\n\n&lt;fieldset&gt;\n  &lt;legend&gt;Prices&lt;\/legend&gt;\n  &lt;app-validation-errors-pane [errors]=&quot;pricesForm().errors()&quot; \/&gt;\n  &lt;table class=&quot;datagrid&quot;&gt;\n    &lt;tr&gt;\n      &lt;th&gt;Flight Class&lt;\/th&gt;\n      &lt;th&gt;Amount&lt;\/th&gt;\n      &lt;th&gt;&lt;\/th&gt;\n    &lt;\/tr&gt;\n    @for (price of pricesForm(); track $index) {\n      &lt;tr&gt;\n        &lt;td&gt;\n          &lt;input [formField]=&quot;price.flightClass&quot; class=&quot;medium&quot; \/&gt;\n        &lt;\/td&gt;\n        &lt;td&gt;\n          &lt;input [formField]=&quot;price.amount&quot; type=&quot;number&quot; class=&quot;small&quot; \/&gt;\n        &lt;\/td&gt;\n        &lt;td class=&quot;error-col&quot;&gt;\n          &lt;app-validation-errors-pane\n            [errors]=&quot;price().errorSummary()&quot;\n            [showFieldNames]=&quot;true&quot;\n          \/&gt;\n        &lt;\/td&gt;\n      &lt;\/tr&gt;\n    }\n  &lt;\/table&gt;\n  &lt;button (click)=&quot;addPrice()&quot; type=&quot;button&quot; class=&quot;btn btn-default ml3&quot;&gt;\n    Add\n  &lt;\/button&gt;\n&lt;\/fieldset&gt;<\/code><\/pre>\n<p>The <code>Add<\/code> button adds a new price to the array. This causes Angular to present fields for it as well:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.html\n\n[...]\n\naddPrice(): void {\n  const prices = this.prices();\n  prices().value.update((prices) =&gt; [...prices, { ...initialPrice }]);\n}<\/code><\/pre>\n<h3>How to Validate Form Arrays in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-validate-form-arrays-in-angular-signal-forms\"><\/a><\/p>\n<p>Validation rules can be defined for all nodes in the object graph shown. Arrays like in our property <code>prices<\/code> are no exception. The following example demonstrates this using a validator that iterates through all prices and checks for duplicates:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-validators.ts\n\nimport { SchemaPath, validate } from &#039;@angular\/forms\/signals&#039;;\nimport { Price } from &#039;.\/price&#039;;\n\nexport function validateDuplicatePrices(path: SchemaPath&lt;Price[]&gt;) {\n  validate(path, (ctx) =&gt; {\n    const prices = ctx.value();\n    const flightClasses = new Set&lt;string&gt;();\n\n    for (const price of prices) {\n      if (flightClasses.has(price.flightClass)) {\n        return {\n          kind: &#039;duplicateFlightClass&#039;,\n          message: &#039;There can only be one price per flight class&#039;,\n          flightClass: price.flightClass,\n        };\n      }\n      flightClasses.add(price.flightClass);\n    }\n\n    return null;\n  });\n}<\/code><\/pre>\n<p>As always, you have to remember to include the validation rule:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { validateDuplicatePrices } from &#039;.\/flight-validators&#039;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  [...]\n  validateDuplicatePrices(path.prices);\n});<\/code><\/pre>\n<h3>How to Split Large Angular Signal Forms into Subforms<\/h3>\n<p><a id=\"how-to-split-large-angular-signal-forms-into-subforms\"><\/a><\/p>\n<p>The code of large forms can quickly become unwieldy. Therefore, it's a good idea to break them down into multiple components. Let's imagine that our flight form is composed of three subforms: a flight form, a prices form, and an aircraft form. The main <code>FlightEdit<\/code> component then simply imports these components:<\/p>\n<pre><code class=\"language-typescript\">\/\/ src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.ts\n[...]\n\nimport { AircraftForm } from &#039;.\/aircraft-form\/aircraft-form&#039;;\nimport { FlightForm } from &#039;.\/flight-form\/flight-form&#039;;\nimport { PricesForm } from &#039;.\/prices-form\/prices-form&#039;;\n\n@Component({\n  selector: &#039;app-flight-edit&#039;,\n  imports: [\n    AircraftForm,\n    PricesForm,\n    FlightForm,\n    ValidationErrorsPane,\n    RouterLink,\n  ],\n  templateUrl: &#039;.\/flight-edit.html&#039;,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class FlightEdit {\n  [...]\n}<\/code><\/pre>\n<p>These components each receive a portion of the entire Signal Form via data binding:<\/p>\n<pre><code class=\"language-html\">&lt;!-- src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.html --&gt;\n\n@if (flight().id !== 0) {\n  &lt;app-validation-errors-pane [errors]=&quot;flightForm().errors()&quot; \/&gt;\n  &lt;form class=&quot;flight-form&quot; novalidate&gt;\n    &lt;app-flight [flight]=&quot;flightForm&quot;&gt;&lt;\/app-flight&gt;\n    &lt;app-prices [prices]=&quot;flightForm.prices&quot;&gt;&lt;\/app-prices&gt;\n    &lt;app-aircraft [aircraft]=&quot;flightForm.aircraft&quot;&gt;&lt;\/app-aircraft&gt;\n\n    &lt;div class=&quot;mt-40&quot;&gt;\n      &lt;button&gt;Save&lt;\/button&gt;\n    &lt;\/div&gt;\n  &lt;\/form&gt;\n}<\/code><\/pre>\n<p>Angular represents these individual parts as <code>FieldTree&lt;T&gt;<\/code>. Inputs must be set up for this:<\/p>\n<pre><code class=\"language-ts\">\/\/ ...\/ticketing\/feature-booking\/flight-edit\/aircraft-form\/aircraft-form.ts\n\nimport { Component, input } from &#039;@angular\/core&#039;;\nimport { FieldTree, FormField } from &#039;@angular\/forms\/signals&#039;;\n\nimport { ValidationErrorsPane } \n  from &#039;..\/..\/..\/..\/shared\/ui-forms\/validation-errors\/validation-errors-pane&#039;;\nimport { Aircraft } from &#039;..\/..\/..\/data\/aircraft&#039;;\n\n@Component({\n  selector: &#039;app-aircraft&#039;,\n  imports: [FormField, ValidationErrorsPane],\n  templateUrl: &#039;.\/aircraft-form.html&#039;,\n})\nexport class AircraftForm {\n  aircraft = input.required&lt;FieldTree&lt;Aircraft&gt;&gt;();\n}<\/code><\/pre>\n<p>For arrays like the <code>price<\/code> array in our example, the same procedure applies:<\/p>\n<pre><code class=\"language-ts\">\/\/ ...\/ticketing\/feature-booking\/flight-edit\/prices-form\/prices-form.ts\n\nimport { Component, input } from &#039;@angular\/core&#039;;\nimport { FieldTree, FormField } from &#039;@angular\/forms\/signals&#039;;\n\nimport { ValidationErrorsPane } \n  from &#039;..\/..\/..\/..\/shared\/ui-forms\/validation-errors\/validation-errors-pane&#039;;\nimport { Price } from &#039;..\/..\/..\/data\/price&#039;;\nimport { initialPrice } from &#039;..\/..\/..\/data\/price-schema&#039;;\n\n@Component({\n  selector: &#039;app-prices&#039;,\n  imports: [FormField, ValidationErrorsPane],\n  templateUrl: &#039;.\/prices-form.html&#039;,\n})\nexport class PricesForm {\n  readonly prices = input.required&lt;FieldTree&lt;Price[]&gt;&gt;();\n\n  addPrice(): void {\n    const prices = this.prices();\n    prices().value.update((prices) =&gt; [...prices, { ...initialPrice }]);\n  }\n}<\/code><\/pre>\n<h2>How Signal Form Metadata Works<\/h2>\n<p><a id=\"how-signal-form-metadata-works\"><\/a><\/p>\n<p>Instead of telling users after they entered something invalid, Signal Forms can provide metadata to let them know upfront which kind of value they are expected to provide:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/11\/G3OgRm2WIAAeX0K.png\" alt=\"Screenshot showing form field metadata such as required markers and length hints in an Angular Signal Form\" \/>{width=50% }<\/p>\n<h3>How to Read Metadata in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-read-metadata-in-angular-signal-forms\"><\/a><\/p>\n<p>Most validators define metadata for the validated fields. To read it, call the <code>metadata<\/code> method with a key, e.g., <code>REQUIRED<\/code> or <code>MIN_LENGTH<\/code>. We move this task into a <code>FieldMetaDataPane<\/code> component that gets a field via an input:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/shared\/ui-forms\/field-meta-data-pane\/field-meta-data-pane.ts\n\nimport { Component, computed, input } from &#039;@angular\/core&#039;;\nimport {\n  FieldTree,\n  REQUIRED,\n  MIN_LENGTH,\n  MAX_LENGTH,\n} from &#039;@angular\/forms\/signals&#039;;\n\n@Component({\n  selector: &#039;app-field-meta-data-pane&#039;,\n  imports: [],\n  templateUrl: &#039;.\/field-meta-data-pane.html&#039;,\n})\nexport class FieldMetaDataPane {\n  readonly field = input.required&lt;FieldTree&lt;unknown&gt;&gt;();\n\n  protected readonly fieldState = computed(() =&gt; this.field()());\n\n  protected readonly isRequired = computed(\n    () =&gt; this.fieldState().metadata(REQUIRED)?.() ?? false,\n  );\n  protected readonly minLength = computed(\n    () =&gt; this.fieldState().metadata(MIN_LENGTH)?.() ?? 0,\n  );\n  protected readonly maxLength = computed(\n    () =&gt; this.fieldState().metadata(MAX_LENGTH)?.() ?? 30,\n  );\n  protected readonly length = computed(\n    () =&gt; <code>(${this.minLength()}..${this.maxLength()})<\/code>,\n  );\n}<\/code><\/pre>\n<p>Here is the template for this component:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/shared\/ui-forms\/field-meta-data-pane\/field-meta-data-pane.html --&gt;\n\n@if (isRequired()) {\n  &lt;span class=&quot;info&quot;&gt;*&lt;\/span&gt;\n}\n&lt;span class=&quot;info info-small&quot;&gt;{{ length() }}&lt;\/span&gt;<\/code><\/pre>\n<h3>How to Display Metadata in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-display-metadata-in-angular-signal-forms\"><\/a><\/p>\n<p>For displaying the metadata, our example imports the <code>FieldMetaDataPane<\/code> into the <code>FlightForm<\/code> component:<\/p>\n<pre><code class=\"language-typescript\">\/\/ ...\/ticketing\/feature-booking\/flight-edit\/flight-form\/flight-form.ts\n\n[...]\n\nimport { FieldMetaDataPane } \n  from &#039;..\/..\/..\/shared\/ui-forms\/field-meta-data-pane\/field-meta-data-pane&#039;;\n\n[...]\n\n@Component({\n  selector: &#039;app-flight-form&#039;,\n  imports: [[...], FieldMetaDataPane],\n  [...]\n})\nexport class FlightForm {\n  [...]\n}<\/code><\/pre>\n<p>In the template, it displays the metadata next to the respective field:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/ticketing\/feature-booking\/flight-edit\/flight-form\/flight-form.html --&gt;\n\n&lt;div class=&quot;form-group&quot;&gt;\n  &lt;label for=&quot;flight-from&quot;&gt;\n    From\n    &lt;app-field-meta-data-pane [field]=&quot;flightForm.from&quot; \/&gt;\n  &lt;\/label&gt;\n  &lt;input class=&quot;form-control&quot; [formField]=&quot;flightForm.from&quot; id=&quot;flight-from&quot; \/&gt;\n  [...]\n&lt;\/div&gt;<\/code><\/pre>\n<h3>How to Define Custom Metadata in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-define-custom-metadata-in-angular-signal-forms\"><\/a><\/p>\n<p>You create your own simple property with <code>createMetadataKey<\/code>:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/shared\/util-common\/properties.ts\n\nimport { createMetadataKey } from &#039;@angular\/forms\/signals&#039;;\n\n\/\/\n\/\/  Property\n\/\/\nexport const CITY = createMetadataKey&lt;boolean&gt;();<\/code><\/pre>\n<p>Now it gets a bit spicy: imagine two validators defining different values for the same property. The first one defines <code>CITY<\/code> as <code>true<\/code>, the second one as <code>false<\/code>. By default, the last defined value wins.<\/p>\n<p>However, using a reducer, you can control how multiple values are combined. For instance, the reducer <code>MetadataReducer.or()<\/code> combines them via a logical OR (<code>value1 || value2<\/code>):<\/p>\n<pre><code class=\"language-ts\">import { createMetadataKey, MetadataReducer } from &#039;@angular\/forms\/signals&#039;;\n\n\/\/\n\/\/  AggregateProperty\n\/\/\nexport const CITY = createMetadataKey(MetadataReducer.or());<\/code><\/pre>\n<p>So, if one validator sets <code>CITY<\/code> to <code>true<\/code>, and another one to <code>false<\/code>, the final value will be <code>true<\/code>.<\/p>\n<p>Besides <code>MetadataReducer.or<\/code>, there are several other reducers such as <code>MetadataReducer.and<\/code>, <code>MetadataReducer.min<\/code>, <code>MetadataReducer.max<\/code>, and <code>MetadataReducer.list<\/code>. The latter puts all values into an array.<\/p>\n<p>For special cases, you can also implement your own reducer by implementing the <code>MetadataReducer&lt;T&gt;<\/code> interface. To illustrate this, here is a custom reducer that also combines boolean values via a logical OR:<\/p>\n<pre><code class=\"language-typescript\">const myOr: MetadataReducer&lt;boolean, boolean&gt; = {\n  reduce(acc, item) {\n    return acc || item;\n  },\n  getInitial() {\n    return false;\n  }\n};\n\nexport const CITY = createMetadataKey(myOr);<\/code><\/pre>\n<p>To define a value for a metadata key, we use the <code>metadata<\/code> function. You can call it directly in the schema, but most likely, you'll call it inside a custom validator where you already define what you expect from the user:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-validators.ts\n\n[...]\n\nexport function validateCityHttp(path: SchemaPathTree&lt;string&gt;) {\n  metadata(path, CITY, () =&gt; true);\n\n  validateHttp(path, { ... });\n}<\/code><\/pre>\n<h3>How to Read Custom Metadata in Angular Signal Forms<\/h3>\n<p><a id=\"how-to-read-custom-metadata-in-angular-signal-forms\"><\/a><\/p>\n<p>Also custom metadata is read by passing the metadata key to the field's <code>metadata<\/code> method:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/shared\/ui-forms\/field-meta-data-pane\/field-meta-data-pane.ts\n\n[...]\n\nimport { CITY } from &#039;..\/..\/util-common\/properties&#039;;\n\n@Component({\n  selector: &#039;app-field-meta-data-pane&#039;,\n  imports: [],\n  templateUrl: &#039;.\/field-meta-data-pane.html&#039;,\n})\nexport class FieldMetaDataPane {\n  readonly field = input.required&lt;FieldTree&lt;unknown&gt;&gt;();\n\n  protected readonly fieldState = computed(() =&gt; this.field()());\n\n  [...]\n\n  protected readonly city = computed(() =&gt; this.fieldState().metadata(CITY));\n}<\/code><\/pre>\n<p>To display our CITY metadata, we also need to extend the <code>FieldMetaDataPane<\/code>'s template:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/shared\/ui-forms\/field-meta-data-pane\/field-meta-data-pane.html --&gt;\n\n[...]\n\n@if (city()) {\n  &lt;span class=&quot;info info-small&quot;&gt;City&lt;\/span&gt;\n}<\/code><\/pre>\n<h2>Why Signal Forms Avoid Undefined Values<\/h2>\n<p><a id=\"why-signal-forms-avoid-undefined-values\"><\/a><\/p>\n<p>It might come as a surprise that Signal Forms do not allow <code>undefined<\/code> values. The reason for this design decision is that <code>undefined<\/code> semantically means that the field does not exist. When initialized with an <code>undefined<\/code> value, the <code>form<\/code> function has no way of knowing that this field is supposed to exist.<\/p>\n<p>To make this clearer, let\u2019s use a thought experiment where the <code>delay<\/code> of our flights is optional:<\/p>\n<pre><code class=\"language-ts\">export interface FlightDomainModel {\n  id: number;\n  from: string;\n  to: string;\n  date: string;\n  delayed: boolean;\n\n  \/\/ Optional delay\n  delay?: number\n}<\/code><\/pre>\n<p>Let's further assume that the backend only expects a <code>delay<\/code> value when <code>delayed<\/code> is <code>true<\/code>. In all other cases, the field is omitted and hence <code>undefined<\/code>. If you now create a form for this model in a component without a <code>delay<\/code>, Angular cannot find the needed metadata for the <code>delay<\/code> field. From its perspective, this field does not exist.<\/p>\n<p>However, from the form's perspective, the field always exists, while sometimes it has no value. This shows that we have a difference between the domain perspective and the form perspective. For this reason, the Angular team recommends distinguishing these different views by defining two separate types:<\/p>\n<pre><code class=\"language-ts\">export interface FlightFormModel {\n  id: number;\n  from: string;\n  to: string;\n  date: string;\n  delayed: boolean;\n  delay: number\n}<\/code><\/pre>\n<p>Instead of <code>undefined<\/code>, Signal Forms would accept <code>null<\/code> as it has the semantics of 'empty value'. However, it is even better to go with a sound default value. In our example, we can simply set the default <code>delay<\/code> to <code>0<\/code>. To bridge between the domain model and the form model, we use mapping functions when creating the Signal Form:<\/p>\n<pre><code class=\"language-ts\">export function toFlightFormModel(model: FlightDomainModel): FlightFormModel {\n  return {\n    ...model,\n    delay: model.delay ?? 0,\n  };\n}<\/code><\/pre>\n<p>For converting back, we have a similar function, given the backend does not accept a delay of <code>0<\/code> when <code>delayed<\/code> is <code>false<\/code>:<\/p>\n<pre><code class=\"language-typescript\">export function toFlightDomainModel(model: FlightFormModel): FlightDomainModel {\n  return {\n    ...model,\n    delay: model.delayed ? model.delay : undefined,\n  };\n}<\/code><\/pre>\n<p>Furthermore, in our component, we can use a linked signal to automatically convert the domain model to the form model within the reactive data flow:<\/p>\n<pre><code class=\"language-typescript\">protected readonly flightDomainModel = signal&lt;FlightDomainModel&gt;({\n  id: 0,\n  from: &#039;&#039;,\n  to: &#039;&#039;,\n  date: &#039;&#039;,\n  delayed: false,\n});\n\nprotected readonly flightFormModel = linkedSignal(\n  () =&gt; toFlightFormModel(this.flightDomainModel())\n);\n\nprotected readonly flightForm = form(this.flightFormModel);<\/code><\/pre>\n<p>When saving the form, we need to convert back to the domain model:<\/p>\n<pre><code class=\"language-ts\">protected save(): void {\n  const formModel = this.flightForm().value();\n  const domainModel = toFlightDomainModel(formModel);\n  [...]\n}<\/code><\/pre>\n<p>When the form model needs to be converted back immediately after typing, a delegated signal, as described in @sec:state-services, can be used.<\/p>\n<p>The same strategy is useful when dealing with fields that appear conditionally. From the form's perspective, these fields always exist, even if they are hidden in the UI. Hence, the correct way for modelling them is to always have them in the form model and to convert them accordingly when mapping to\/from the domain model.<\/p>\n<h2>How to Build Custom Controls for Signal Forms<\/h2>\n<p><a id=\"how-to-build-custom-controls-for-signal-forms\"><\/a><\/p>\n<p>So far, we've only used the <code>FormField<\/code> directive with standard HTML elements. The question now is how to make this directive work with our own widgets. For example, let's imagine a <code>DelayStepper<\/code> component to change the flight delay in 15-minute increments. This should be able to be bound to a field with <code>FormField<\/code>, just like all other fields:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/ticketing\/feature-booking\/flight-edit\/flight-form\/flight-form.html --&gt;\n\n&lt;div class=&quot;form-group form-check&quot;&gt;\n  &lt;label for=&quot;delay&quot;&gt;Delay&lt;\/label&gt;\n  &lt;app-delay-stepper id=&quot;delay&quot; [formField]=&quot;flightForm.delay&quot; \/&gt;\n  &lt;app-validation-errors-pane [errors]=&quot;flightForm.delay().errors()&quot; \/&gt;\n&lt;\/div&gt;<\/code><\/pre>\n<p>In the past, such widgets had to be provided with a so-called Control Value Accessor. Unfortunately, this wasn't exactly straightforward, and the chance is quite low that the Control Value Accessor was ever on the top ten of the most popular Angular concepts.<\/p>\n<p>Signal Forms makes the whole process much simpler: The widget implements the <code>FormValueControl&lt;T&gt;<\/code> interface, which simply enforces a <code>ModelSignal<\/code> named <code>value<\/code>. It also defines other optional properties, such as <code>disabled<\/code> or <code>errors<\/code>, which the widget can access as needed:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/shared\/ui-common\/delay-stepper\/delay-stepper.ts\n\nimport { Component, effect, input, model } from &#039;@angular\/core&#039;;\nimport { FormValueControl, ValidationError } from &#039;@angular\/forms\/signals&#039;;\n\n@Component({\n  selector: &#039;app-delay-stepper&#039;,\n  imports: [],\n  templateUrl: &#039;.\/delay-stepper.html&#039;,\n})\nexport class DelayStepper implements FormValueControl&lt;number&gt; {\n  readonly value = model(0);\n\n  readonly disabled = input(false);\n  readonly errors = input&lt;readonly ValidationError.WithOptionalField[]&gt;([]);\n\n  constructor() {\n    effect(() =&gt; {\n      console.log(&#039;DelayStepper, errors&#039;, this.errors());\n    });\n  }\n\n  protected inc(): void {\n    this.value.update((v) =&gt; v + 15);\n  }\n\n  protected dec(): void {\n    this.value.update((v) =&gt; Math.max(v - 15, 0));\n  }\n}<\/code><\/pre>\n<p>The <code>DelayStepper<\/code> component uses the <code>disabled<\/code> property to indicate whether the field is disabled due to a schema rule. It receives all validation errors from Signal Forms via the input <code>errors<\/code>. An effect writes this property to the console for demonstration purposes. The <code>disabled<\/code> property is used alongside the <code>value<\/code> in the template:<\/p>\n<pre><code class=\"language-html\">&lt;!-- src\/app\/domains\/shared\/ui-common\/delay-stepper\/delay-stepper.html --&gt;\n\n@if (disabled()) {\n  &lt;div class=&quot;delay&quot;&gt;No Delay!&lt;\/div&gt;\n} @else {\n  &lt;div class=&quot;delay&quot;&gt;{{ value() }}&lt;\/div&gt;\n  &lt;div&gt;\n    &lt;button type=&quot;button&quot; (click)=&quot;inc()&quot;&gt;+15 Minutes&lt;\/button&gt; |\n    &lt;button type=&quot;button&quot; (click)=&quot;dec()&quot;&gt;-15 Minutes&lt;\/button&gt;\n  &lt;\/div&gt;\n}<\/code><\/pre>\n<p><strong>Note on Custom Checkboxes<\/strong><\/p>\n<p><code>FormValueControl<\/code> can also be used for checkboxes, as it provides an optional <code>checked<\/code> property. However, for this purpose, Signal Forms also provides a specialized <code>FormCheckboxControl<\/code> interface. It defines a mandatory <code>checked<\/code> property and an optional <code>value<\/code>.<\/p>\n<h2>Angular Signal Forms FAQ<\/h2>\n<p><a id=\"angular-signal-forms-faq\"><\/a><\/p>\n<h3>What are Angular Signal Forms?<\/h3>\n<p><a id=\"what-are-angular-signal-forms\"><\/a><\/p>\n<p>Angular Signal Forms are a forms API in <a href=\"mailto:code&gt;@angular\/forms\/signals&lt;\/code\">code>@angular\/forms\/signals<\/code<\/a> that model form values, validation, metadata, and submission state with Signals. This keeps forms aligned with Angular's signal-based reactivity instead of introducing a separate abstraction layer.<\/p>\n<h3>How are Signal Forms different from Angular Reactive Forms?<\/h3>\n<p><a id=\"how-are-signal-forms-different-from-angular-reactive-forms\"><\/a><\/p>\n<p>Reactive Forms revolve around classes such as <code>FormGroup<\/code>, <code>FormControl<\/code>, and <code>FormArray<\/code>. Signal Forms instead expose a <code>FieldTree<\/code> whose state is read through Signals, which makes derived UI state, validation feedback, and composition fit more naturally into signal-first Angular applications.<\/p>\n<h3>Do Signal Forms support async validation and nested forms?<\/h3>\n<p><a id=\"do-signal-forms-support-async-validation-and-nested-forms\"><\/a><\/p>\n<p>Yes. Signal Forms support asynchronous validators, cross-field validation, nested objects, form arrays, and subforms. This makes them suitable for both simple inputs and larger business forms with complex validation rules.<\/p>\n<h3>Why should form models avoid <code>undefined<\/code> values?<\/h3>\n<p><a id=\"why-should-form-models-avoid-undefined-values\"><\/a><\/p>\n<p>Signal Forms interpret <code>undefined<\/code> as a missing field, not as an empty value. For optional data, it is usually better to model a concrete form value such as <code>null<\/code> or a domain-specific default and map it back to the domain model when saving.<\/p>\n<h2>Summary<\/h2>\n<p><a id=\"summary\"><\/a><\/p>\n<p>Signal Forms introduce a signal-based model for building and validating forms in Angular. Form state, values, and validation results are represented as signals, making them fully reactive and composable. This unifies user interaction and application state under a single set of reactive primitives.<\/p>\n<p>Validation is defined declaratively through schemas that can be composed, reused, and applied conditionally. Signal Forms support built-in, custom, multi-field, and asynchronous validators, as well as integration with external schema standards such as Zod. Errors, pending states, and validation metadata are all part of the reactive form state and can be consumed directly by the UI.<\/p>\n<p>Complex forms are modeled using nested objects, arrays, and subforms, allowing large forms to be split into focused components. A clear separation between domain models and form models avoids issues with optional or undefined values. Custom form controls integrate through a simple signal-based interface, making advanced UI widgets first-class citizens in the form system.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is post 2 of 3 in the series &ldquo;Signal Forms&rdquo; Dynamic Forms: Building a Form Generator with Signal Forms Angular Signal Forms &#8211; Everything You Need to Know Migrating to Angular Signal Forms: Interop with Reactive Forms Last Update: April 2026 Angular Signals make component state predictable and reactive, but classic form APIs still [&hellip;]<\/p>\n","protected":false},"author":25,"featured_media":31226,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_price":"","_stock":"","_tribe_ticket_header":"","_tribe_default_ticket_provider":"","_ticket_start_date":"","_ticket_end_date":"","_tribe_ticket_show_description":"","_tribe_ticket_show_not_going":false,"_tribe_ticket_use_global_stock":"","_tribe_ticket_global_stock_level":"","_global_stock_mode":"","_global_stock_cap":"","_tribe_rsvp_for_event":"","_tribe_ticket_going_count":"","_tribe_ticket_not_going_count":"","_tribe_tickets_list":"[]","_tribe_ticket_has_attendee_info_fields":false,"footnotes":""},"categories":[18],"tags":[],"class_list":["post-31238","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","post_series-signal-forms-en"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.1.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Angular Signal Forms - Everything You Need to Know - ANGULARarchitects<\/title>\n<meta name=\"description\" content=\"All about Angular Signal Forms: Custom Validation, Schemas, Nested Forms, Form Array and Form Control, Metadata, Debouncing, Custom Controls\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Angular Signal Forms - Everything You Need to Know - ANGULARarchitects\" \/>\n<meta property=\"og:description\" content=\"All about Angular Signal Forms: Custom Validation, Schemas, Nested Forms, Form Array and Form Control, Metadata, Debouncing, Custom Controls\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/\" \/>\n<meta property=\"og:site_name\" content=\"ANGULARarchitects\" \/>\n<meta property=\"article:published_time\" content=\"2026-01-22T20:00:49+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-04-20T18:44:41+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/sujet-1-1024x536.png\" \/>\n<meta name=\"author\" content=\"Manfred Steyer\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/sujet-1-1024x536.png\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Manfred Steyer\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/\"},\"author\":{\"name\":\"Manfred Steyer\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a\"},\"headline\":\"Angular Signal Forms &#8211; Everything You Need to Know\",\"datePublished\":\"2026-01-22T20:00:49+00:00\",\"dateModified\":\"2026-04-20T18:44:41+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/\"},\"wordCount\":200,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/shutterstock_2631518239.jpg\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/\",\"name\":\"Angular Signal Forms - Everything You Need to Know - ANGULARarchitects\",\"isPartOf\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/shutterstock_2631518239.jpg\",\"datePublished\":\"2026-01-22T20:00:49+00:00\",\"dateModified\":\"2026-04-20T18:44:41+00:00\",\"description\":\"All about Angular Signal Forms: Custom Validation, Schemas, Nested Forms, Form Array and Form Control, Metadata, Debouncing, Custom Controls\",\"breadcrumb\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#primaryimage\",\"url\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/shutterstock_2631518239.jpg\",\"contentUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/shutterstock_2631518239.jpg\",\"width\":1000,\"height\":677},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.angulararchitects.io\/en\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Angular Signal Forms &#8211; Everything You Need to Know\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#website\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/\",\"name\":\"ANGULARarchitects\",\"description\":\"AngularArchitects.io\",\"publisher\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.angulararchitects.io\/en\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#organization\",\"name\":\"ANGULARarchitects\",\"alternateName\":\"SOFTWAREarchitects\",\"url\":\"https:\/\/www.angulararchitects.io\/en\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg\",\"contentUrl\":\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg\",\"width\":644,\"height\":216,\"caption\":\"ANGULARarchitects\"},\"image\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/github.com\/angular-architects\",\"https:\/\/www.linkedin.com\/company\/angular-architects\/\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a\",\"name\":\"Manfred Steyer\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g\",\"caption\":\"Manfred Steyer\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Angular Signal Forms - Everything You Need to Know - ANGULARarchitects","description":"All about Angular Signal Forms: Custom Validation, Schemas, Nested Forms, Form Array and Form Control, Metadata, Debouncing, Custom Controls","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/","og_locale":"en_US","og_type":"article","og_title":"Angular Signal Forms - Everything You Need to Know - ANGULARarchitects","og_description":"All about Angular Signal Forms: Custom Validation, Schemas, Nested Forms, Form Array and Form Control, Metadata, Debouncing, Custom Controls","og_url":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/","og_site_name":"ANGULARarchitects","article_published_time":"2026-01-22T20:00:49+00:00","article_modified_time":"2026-04-20T18:44:41+00:00","og_image":[{"url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/sujet-1-1024x536.png","type":"","width":"","height":""}],"author":"Manfred Steyer","twitter_card":"summary_large_image","twitter_image":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/sujet-1-1024x536.png","twitter_misc":{"Written by":"Manfred Steyer","Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#article","isPartOf":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/"},"author":{"name":"Manfred Steyer","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a"},"headline":"Angular Signal Forms &#8211; Everything You Need to Know","datePublished":"2026-01-22T20:00:49+00:00","dateModified":"2026-04-20T18:44:41+00:00","mainEntityOfPage":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/"},"wordCount":200,"commentCount":0,"publisher":{"@id":"https:\/\/www.angulararchitects.io\/en\/#organization"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#primaryimage"},"thumbnailUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/shutterstock_2631518239.jpg","inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/","url":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/","name":"Angular Signal Forms - Everything You Need to Know - ANGULARarchitects","isPartOf":{"@id":"https:\/\/www.angulararchitects.io\/en\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#primaryimage"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#primaryimage"},"thumbnailUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/shutterstock_2631518239.jpg","datePublished":"2026-01-22T20:00:49+00:00","dateModified":"2026-04-20T18:44:41+00:00","description":"All about Angular Signal Forms: Custom Validation, Schemas, Nested Forms, Form Array and Form Control, Metadata, Debouncing, Custom Controls","breadcrumb":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#primaryimage","url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/shutterstock_2631518239.jpg","contentUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/shutterstock_2631518239.jpg","width":1000,"height":677},{"@type":"BreadcrumbList","@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.angulararchitects.io\/en\/"},{"@type":"ListItem","position":2,"name":"Angular Signal Forms &#8211; Everything You Need to Know"}]},{"@type":"WebSite","@id":"https:\/\/www.angulararchitects.io\/en\/#website","url":"https:\/\/www.angulararchitects.io\/en\/","name":"ANGULARarchitects","description":"AngularArchitects.io","publisher":{"@id":"https:\/\/www.angulararchitects.io\/en\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.angulararchitects.io\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.angulararchitects.io\/en\/#organization","name":"ANGULARarchitects","alternateName":"SOFTWAREarchitects","url":"https:\/\/www.angulararchitects.io\/en\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/","url":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg","contentUrl":"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2023\/07\/AA-Logo-RGB-horizontal-inside-knowledge-black.svg","width":644,"height":216,"caption":"ANGULARarchitects"},"image":{"@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/github.com\/angular-architects","https:\/\/www.linkedin.com\/company\/angular-architects\/"]},{"@type":"Person","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/f3de69c1e2bdb5ba04d8d2f5f998b81a","name":"Manfred Steyer","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.angulararchitects.io\/en\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/8778dfb353992fa3a0d909beee085a088891e5bfce65cdb3631801da527cf11b?s=96&d=mm&r=g","caption":"Manfred Steyer"}}]}},"_links":{"self":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/31238","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/users\/25"}],"replies":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/comments?post=31238"}],"version-history":[{"count":10,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/31238\/revisions"}],"predecessor-version":[{"id":33238,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/31238\/revisions\/33238"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/media\/31226"}],"wp:attachment":[{"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/media?parent=31238"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/categories?post=31238"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/tags?post=31238"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}