{"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-02-14T13:24:37","modified_gmt":"2026-02-14T12:24:37","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-en69df40461256e\" 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-en69df40461256e\"\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>The long-awaited Signal Forms bridge a crucial gap between Angular's Signal-based reactivity and user interaction. While currently experimental and intended to gather initial feedback, they show the direction Angular is heading.<\/p>\n<p>In this article, I'll go into the details of this new library. In addition to the basics, I'll discuss more advanced aspects such as the many types of custom validators, conditional validation, subforms, and custom controls.<\/p>\n<p><strong>Table of Contents<\/strong><\/p>\n<ol>\n<li><a href=\"#first-signal-form\">Creating a Signal Form<\/a>  <\/li>\n<li><a href=\"#field-tree\">Understanding the FieldTree Type<\/a><\/li>\n<li><a href=\"#working-with-schemas\">Signal Form Schemas<\/a>  <\/li>\n<li><a href=\"#validation\">Field Validation<\/a>: <a href=\"#implementing-custom-validators\">Custom<\/a>, <a href=\"#conditional-validation\">Conditional<\/a>, <a href=\"#multi-field-validators\">Cross-Field<\/a>, <a href=\"#asynchronous-validators\">Async<\/a><\/li>\n<li><a href=\"#large-nested-forms\">Nested Forms and Form Arrays<\/a>  <\/li>\n<li><a href=\"#working-with-form-metadata\">Form Metadata<\/a>  <\/li>\n<li><a href=\"#null-and-undefined-values\">Null and Undefined Values<\/a><\/li>\n<li><a href=\"#custom-fields\">Creating Custom Fields<\/a>  <\/li>\n<\/ol>\n<h2>Creating a Signal Form <span id=\"first-signal-form\"><\/span><\/h2>\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<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/01\/solution2.png\" alt=\"The FlightEdit component covers most Signal Forms features\" \/><\/p>\n<h3>Setting up the Component<\/h3>\n<p>Our implementation receives the flight to be displayed from a service named <code>FlightDetailStore<\/code>. To set up our Signal Form, we pass this flight to the <code>form<\/code> function provided by Angular's Signal Forms package :<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.ts\n\nimport { linkedSignal } from &#039;@angular\/core&#039;;\nimport { form, minLength, required } from &#039;@angular\/forms\/signals&#039;;\n[...]\n\n@Component([...])\nexport class FlightEditComponent {\n  private readonly store = inject(FlightDetailStore);\n\n  protected readonly flight = linkedSignal(() =&gt; this.store.flightValue());\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>As the service returns a readonly signal, we use <code>linkedSignal<\/code> to convert it in a writable one. This is important because Signal Forms writes back the modifications performed by the user.<\/p>\n<p>The second parameter <code>form<\/code> gets passed 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 point to 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 to bind the individual properties of the flight to form controls in the template. We'll discuss this imporant data structure in the next section.<\/p>\n<h3>Understanding the FieldTree Type <span id=\"field-tree\"><\/span><\/h3>\n<p>You can think about 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 = flightForm.date().value(); \nconst isDateDirty = flightForm.date().dirty();\nconst isDateInvalid = flightForm.date().invalid();\nconst dateErrors = 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, our schema set up 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 = flightForm.aircraft.type().value();\nconst isAircraftTypeDirty = flightForm.aircraft.type().dirty();\nconst isAircraftTypeInvalid = flightForm.aircraft.type().invalid();\nconst aircraftTypeErrors = flightForm.aircraft.type().errors();\n\nconst firstPriceAmount = this.flightForm.prices[0].amount().value();\nconst isFirstPriceAmountDirty = this.flightForm.prices[0].amount().dirty();\nconst isFirstPriceAmountInvalid = this.flightForm.prices[0].amount().invalid();\nconst firstPriceAmountErrors = this.flightForm.prices[0].amount().errors();<\/code><\/pre>\n<p>For our example that means that we can bind the properties of the flight itself but also the properties of the connected airplane and the connected prices. The next section is already binding some properties to the template.<\/p>\n<h3>Binding to the Template<\/h3>\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 FlightEditComponent {\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> or <code>select<\/code> but also to custom controls implemented with Angular as we will see at the end of this article.<\/p>\n<p>For now, let's just bind some properties of our <code>flight<\/code> to input fields:<\/p>\n<pre><code class=\"language-html\">&lt;!-- ...\/ticketing\/feature-booking\/flight-edit\/flight-form\/flight-form.html --&gt;\n\n@let flightForm = flight();\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.id().invalid()) {\n      &lt;div&gt;{{ flightForm.id().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>Behind <code>errors<\/code> 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><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2026\/02\/min-length.png\" style=\"max-width:600px; width:100%\"><\/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<h3>Submitting  <span id=\"submitting-form\"><\/span><\/h3>\n<p>In general, you don't need any special function to submit a form. You can simply read the form's value and pass it to a service or store. However, the <code>submit<\/code> function provided by Signal Forms offers additional benefits:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-edit.ts\n\n[...]\n\n\/\/ Add submit\nimport {\n  form,\n  FormField,\n  minLength,\n  required,\n  submit,\n} from &#039;@angular\/forms\/signals&#039;;\n\n[...]\n\n@Component({\n  [...]\n})\nexport class FlightEditComponent {\n\n  [...]\n\n  protected readonly flightForm = form(this.flight, [...]);\n\n  [...]\n\n  protected async save(): Promise&lt;void&gt; {\n    await submit(this.flightForm, async (form) =&gt; {\n      try {\n        await this.store.saveFlight(form().value());\n        return null;\n      } catch (error) {\n        return {\n          kind: &quot;processing_error&quot;,\n          error: String(error),\n        };\n      }\n    });\n  }\n}<\/code><\/pre>\n<p>The <code>submit<\/code> function accepts both a signal form and an action to be executed. The latter is only executed if there are no validation errors. If an error occurs during saving, this action can return a <code>ValidationError<\/code>. Alternatively, it could return an array containing multiple <code>ValidationError<\/code> objects.<\/p>\n<p>The function <code>submit<\/code> adds these errors in the Signal Form. Hence, they can be displayed like any other validation errors by presenting the form element's <code>errors<\/code> array.<\/p>\n<div style=\"\nmargin: 28px 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>This article is extracted from my new book <a href=\"https:\/\/www.angulararchitects.io\/en\/ebooks\/modern\/\">Modern Angular - Architecture, Concepts, Implementation<\/a>. This book covers everything you need for building modern business applications with Angular: from Signals and state patterns to architecture, AI assistants, testing, and practical solutions for real-world projects.<\/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 - Architecture, Concepts, Implementation\" 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>Schemas \u2014 How They Work? <span id=\"working-with-schemas\"><\/span><\/h2>\n<p>The schema passed to the <em>form<\/em> function defines not only validation rules but also other aspects of the form's behavior. For instance, a schema 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>Splitting Schemas for Maintainability <span id=\"using-separate-schemas\"><\/span><\/h3>\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 separate 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 &quot;@angular\/forms\/signals&quot;;\n\nimport { Flight } from &quot;.\/flight&quot;;\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\n[...]\n\nprotected readonly flightForm = form(this.flight, flightSchema);\n\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>Another case where schemas refer to other schemas is conditional validation rules. Below, we will discuss this.<\/p>\n<h3>Controlling Form Behavior <span id=\"controlling-behavior\"><\/span><\/h3>\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[...]\n\ndisabled(path.delay, (ctx) =&gt; !ctx.valueOf(path.delayed));\n\n[...]<\/code><\/pre>\n<p>Instead of <code>true<\/code>, you can also return a reason for the disabling:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\ndisabled(path.delay, (ctx) =&gt;\n  !ctx.valueOf(path.delayed) ? &quot;not delayed&quot; : false,\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\">&lt;!-- src\/app\/domains\/ticketing\/feature-booking\/flight-edit\/flight-form\/flight-form.html --&gt;\n\n@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 alway 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 usually, hiding a field also requires hiding its label and perhaps other UI elements. Hence, hidden is only a hint 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>Debouncing in Signal Forms Explained <span id=\"debouncing\"><\/span><\/h3>\n<p>Especially in reactive UIs with search inputs, debouncing is essential. Instead of firing an HTTP request on every keystroke, we wait until the user has stopped typing for a few milliseconds. In our example, this behavior appears in the flight search form:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/11\/search.png\" style=\"width:100%; max-width:600px\" alt=\"Reactive Search Form\"><\/p>\n<p>The form's behavior for debouncing is specified in its schema. For this, the <code>debounce<\/code> function can be used:<\/p>\n<pre><code class=\"language-ts\">\/\/ ...\/feature-booking\/reactive-flight-search\/reactive-flight-search.ts\n\n[...]\n\nimport { debounce, form, minLength, required } from &quot;@angular\/forms\/signals&quot;;\n\n[...]\n\nprotected readonly filterForm = form(this.filter, (schema) =&gt; {\n  debounce(schema, 300);\n\n  required(schema.from);\n  minLength(schema.from, 3);\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 respective debouncer function:<\/p>\n<pre><code class=\"language-ts\">filterForm = form(this.filter, (schema) =&gt; {\n  debounce(schema, (ctx, _abortSignal) =&gt; {\n    return new Promise((resolve) =&gt; {\n      setTimeout(resolve, 300);\n    });\n  });\n\n  required(schema.from);\n  minLength(schema.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>Validating Against Zod and Standard Schema <span id=\"validating-zod-standard-schema\"><\/span><\/h3>\n<p>Often, there is already something, such as a Zod schema, that describes rules for valid objects. Perhaps you already have them for server-side code or you generate it from a JSON Schema or an Open API document. Fortunately, you can directly validate against such schemas:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { validateStandardSchema, schema } from &quot;@angular\/forms\/signals&quot;;\nimport { FlightZodSchema } from &quot;.\/flight-zod-schema&quot;;\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.<\/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>Field Validation <span id=\"validation\"><\/span><\/h2>\n<p>Validation is a crucial aspect of form handling. Signal Forms provide a comprehensive validation system that goes far beyond simple required fields. You can create custom validators, implement conditional validation rules, validate multiple fields together, and even perform asynchronous validation via HTTP calls. This section explores the various validation strategies available in Signal Forms.<\/p>\n<h3>Implementing Custom Validators for Signal Forms <span id=\"implementing-custom-validators\"><\/span><\/h3>\n<p>Custom validators allow you to implement validation logic that goes beyond the built-in validators. They can check against business rules and compare values. To set them up, 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\">import { validate } from &quot;@angular\/forms\/signals&quot;;\n\n[...]\n\nprotected readonly flightForm = form(this.flight, (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> that identifies the validation error with a string <code>kind<\/code>. The validator can provide more detailed error information using additional properties it can freely specify. 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>Refactoring Validators into Functions <span id=\"refactoring-validators-into-functions\"><\/span><\/h3>\n<p>To improve clarity and reuse, it is advisable to refactor custom validators to separate functions. They must at least accept the property to be validated. The type <code>SchemaPathTree&lt;T&gt;<\/code> is used for this purpose. A <code>SchemaPathTree&lt;string&gt;<\/code>, for example, refers to a property of type <code>string<\/code>:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-validators.ts\n\nimport { SchemaPathTree, validate } from &quot;@angular\/forms\/signals&quot;;\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: &quot;city&quot;,\n      value,\n      allowed,\n    };\n  });\n}<\/code><\/pre>\n<p>The validator shown here accepts the airports 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 &quot;.\/flight-validators&quot;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  [...]\n  validateCity(path.from, [&quot;Graz&quot;, &quot;Hamburg&quot;, &quot;Z\u00fcrich&quot;]);\n});<\/code><\/pre>\n<h3>Showing Validation Errors <span id=\"showing-validation-errors\"><\/span><\/h3>\n<p>So far, we've only returned the <code>errors<\/code> object to indicate 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: &quot;Please enter a value!&quot; });<\/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. 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 &quot;@angular\/core&quot;;\nimport { MinValidationError, ValidationError } from &quot;@angular\/forms\/signals&quot;;\n\n@Component({\n  selector: &quot;app-validation-errors-pane&quot;,\n  imports: [],\n  templateUrl: &quot;.\/validation-errors-pane.html&quot;,\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) + &quot;: &quot; : &quot;&quot;;\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(&quot;.&quot;).at(-1);\n}\n\nfunction toMessage(error: ValidationError): string {\n  switch (error.kind) {\n    case &quot;required&quot;:\n      return &quot;Enter a value!&quot;;\n    case &quot;roundtrip&quot;:\n    case &quot;roundtrip_tree&quot;:\n      return &quot;Roundtrips are not supported!&quot;;\n    case &quot;min&quot;:\n      return <code>Minimum amount: ${(error as MinValidationError).min}<\/code>;\n    default:\n      return error.kind ?? &quot;Validation Error&quot;;\n  }\n}<\/code><\/pre>\n<p>The template displays the error messages obtained in this way:<\/p>\n<pre><code class=\"language-html\">&lt;!-- src\/app\/domains\/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>The consumer must now delegate to the <code>ValidationErrorsPane<\/code> component for each field and pass the <code>errors<\/code> array:<\/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;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>Conditional Validation <span id=\"conditional-validation\"><\/span><\/h3>\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 the path to be validated, a predicate \u2014 typically a lambda expression \u2014 and a schema. If the predicate returns <code>true<\/code>, <code>applyWhenValue<\/code> applies the schema:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { applyWhenValue, required, min, schema } from &quot;@angular\/forms\/signals&quot;;\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 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 stateOf 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> or <code>touched<\/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>Cross-Field Validation in Signal Forms <span id=\"multi-field-validators\"><\/span><\/h3>\n<p>Sometimes we need to respect 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 &quot;@angular\/forms\/signals&quot;;\nimport { Flight } from &quot;.\/flight&quot;;\n\nexport function validateRoundTrip(schema: SchemaPathTree&lt;Flight&gt;) {\n  validate(schema, (ctx) =&gt; {\n    const from = ctx.fieldTree.from().value();\n    const to = ctx.fieldTree.to().value();\n\n    \/\/ Alternative:\n    \/\/ const from = ctx.valueOf(schema.from);\n    \/\/ const to = ctx.valueOf(schema.to);\n\n    if (from === to) {\n      return {\n        kind: &quot;roundtrip&quot;,\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\nimport { validateRoundTrip } from &quot;.\/flight-validators&quot;;\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  \/\/ ...\n  validateRoundTrip(path);\n});<\/code><\/pre>\n<p>Validators always return error messages for the validated level. In this case, this is the <code>flight<\/code> itself, not <code>from<\/code> or <code>to<\/code>. Therefore, the application also uses the <code>flight<\/code>'s <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\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 [errors]=&quot;flightForm().errorSummary()&quot; \/&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. This information can also be found in the individual <code>ValidationError<\/code> objects. The <code>ValidationErrorsPane<\/code> component in my <a href=\"https:\/\/github.com\/manfredsteyer\/modern\/tree\/signal-forms-example\">examples<\/a> (see branch <code>signal-forms-example<\/code>), which is somewhat more comprehensive than the variant discussed here, takes advantage of this option.<\/p>\n<h3>Accessing Sibling Fields <span id=\"accessing-sibling-fields\"><\/span><\/h3>\n<p>An alternative implementation of the roundtrip 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(schema: SchemaPathTree&lt;Flight&gt;) {\n  \/\/ Now, we are validating the &#039;from&#039; field only\n  validate(schema.from, (ctx) =&gt; {\n    const from = ctx.value();\n    const to = ctx.valueOf(schema.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>Tree Validators <span id=\"tree-validators\"><\/span><\/h3>\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 &quot;@angular\/forms\/signals&quot;;\nimport { Flight } from &quot;.\/flight&quot;;\n\nexport function validateRoundTripTree(schema: SchemaPathTree&lt;Flight&gt;) {\n  validateTree(schema, (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: &quot;roundtrip_tree&quot;,\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 field level:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/app\/domains\/ticketing\/data\/flight-schema.ts\n\nimport { validateRoundTripTree } from &quot;.\/flight-validators&quot;;\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  \/\/ ...\n  validateRoundTripTree(path);\n});<\/code><\/pre>\n<p>In the initial screenshot shown above, this error message is the second one from the top.<\/p>\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:<\/p>\n<pre><code class=\"language-ts\">validate(path.to, (ctx) =&gt; {\n  if (ctx.value() === ctx.valueOf(path.from)) {\n    return { kind: &quot;roundtrip&quot; };\n  }\n  return null;\n});<\/code><\/pre>\n<h3>Asynchronous Validators for Signal Forms <span id=\"asynchronous-validators\"><\/span><\/h3>\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 listing demonstrates this using an <code>rxResource<\/code> that uses 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 &quot;@angular\/core\/rxjs-interop&quot;;\nimport { SchemaPathTree, validateAsync, metadata } from &quot;@angular\/forms\/signals&quot;;\nimport { delay, map, Observable, of } from &quot;rxjs&quot;;\n\nexport function validateCityAsync(schema: SchemaPathTree&lt;string&gt;) {\n  validateAsync(schema, {\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: &quot;airport_not_found_http&quot;,\n        };\n      }\n      return null;\n    },\n    onError: (error, _ctx) =&gt; {\n      console.error(&quot;api error validating city&quot;, error);\n      return {\n        kind: &quot;api-failed&quot;,\n      };\n    },\n  });\n}\n\n\/\/ Simulates a server-side validation\nfunction rxValidateAirport(airport: string): Observable&lt;boolean&gt; {\n  const allowed = [&quot;Graz&quot;, &quot;Hamburg&quot;, &quot;Z\u00fcrich&quot;];\n  return of(null).pipe(\n    delay(2000),\n    map(() =&gt; allowed.includes(airport)),\n  );\n}<\/code><\/pre>\n<p>As usual, 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 &quot;.\/flight-validators&quot;;\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>HTTP Validators <span id=\"http-validators\"><\/span><\/h3>\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 &quot;@angular\/forms\/signals&quot;;\nimport { Flight } from &quot;.\/flight&quot;;\nimport { CITY } from &quot;..\/..\/shared\/util-common\/properties&quot;;\n\nexport function validateCityHttp(schema: SchemaPathTree&lt;string&gt;) {\n  metadata(schema, CITY, () =&gt; true);\n\n  validateHttp(schema, {\n    request: (ctx) =&gt; ({\n      url: &quot;https:\/\/demo.angulararchitects.io\/api\/flight&quot;,\n      params: {\n        from: ctx.value(),\n      },\n    }),\n    onSuccess: (result: Flight[], _ctx) =&gt; {\n      if (result.length === 0) {\n        return {\n          kind: &quot;airport_not_found_http&quot;,\n        };\n      }\n      return null;\n    },\n    onError: (error, _ctx) =&gt; {\n      console.error(&quot;api error validating city&quot;, error);\n      return {\n        kind: &quot;api-failed&quot;,\n      };\n    },\n  });\n}<\/code><\/pre>\n<p>As usual, <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 &quot;.\/flight-validators&quot;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  [...]\n  validateCityHttp(path.to);\n});<\/code><\/pre>\n<h2>Nested Forms and Form Arrays <span id=\"large-nested-forms\"><\/span><\/h2>\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>Form Groups <span id=\"form-groups\"><\/span><\/h3>\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 &quot;@angular\/forms\/signals&quot;;\nimport { Aircraft } from &quot;.\/aircraft&quot;;\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 &quot;@angular\/forms\/signals&quot;;\nimport { aircraftSchema } from &quot;.\/aircraft-schema&quot;;\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 <code>let<\/code>:<\/p>\n<pre><code class=\"language-html\">&lt;!-- \n ...\/ticketing\/feature-booking\/flight-edit\/aircraft-form\/aircraft-form.html \n--&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  <span id=\"form-arrays\"><\/span><\/h3>\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 &quot;@angular\/forms\/signals&quot;;\nimport { Price } from &quot;.\/price&quot;;\n\nexport const initPrice: Price = {\n  flightClass: &quot;&quot;,\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 &quot;@angular\/forms\/signals&quot;;\nimport { priceSchema } from &quot;.\/price-schema&quot;;\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;!-- ...\/ticketing\/feature-booking\/flight-edit\/prices-form\/prices-form.html --&gt;\n\n@let priceForms = prices();\n\n&lt;fieldset&gt;\n  &lt;legend&gt;Prices&lt;\/legend&gt;\n  &lt;app-validation-errors-pane [errors]=&quot;priceForms().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 priceForms; 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\">addPrice(): void {\n  const pricesForms = this.prices();\n  pricesForms().value.update((prices) =&gt; [...prices, { ...initPrice }]);\n}<\/code><\/pre>\n<h3>Validating Form Arrays <span id=\"validating-form-arrays\"><\/span><\/h3>\n<p>Validation rules can be defined for all nodes in the object graph shown. Arrays like our prices are no exception. The following listing 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 &quot;@angular\/forms\/signals&quot;;\nimport { Price } from &quot;.\/price&quot;;\n\nexport function validateDuplicatePrices(prices: SchemaPath&lt;Price[]&gt;) {\n  validate(prices, (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: &quot;duplicateFlightClass&quot;,\n          message: &quot;There can only be one price per flight class&quot;,\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 &quot;.\/flight-validators&quot;;\n\n[...]\n\nexport const flightSchema = schema&lt;Flight&gt;((path) =&gt; {\n  [...]\n  validateDuplicatePrices(path.prices);\n});<\/code><\/pre>\n<h3>Nested Forms (Subforms) <span id=\"subforms\"><\/span><\/h3>\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 types: the flight itself, an aircraft, and prices: <\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/09\/model.png\" alt=\"Flights with Aircraft and Prices\" style=\"width:100%; max-width:600px\"><\/p>\n<p>For these parts, we could define subforms. The main <code>FlightEdit<\/code> component then 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 type=&quot;button&quot; (click)=&quot;save()&quot;&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 &quot;@angular\/core&quot;;\nimport { FieldTree, FormField } from &quot;@angular\/forms\/signals&quot;;\n\nimport { ValidationErrorsPane } \n  from &quot;..\/..\/..\/..\/shared\/ui-forms\/validation-errors\/validation-errors-pane&quot;;\nimport { Aircraft } from &quot;..\/..\/..\/data\/aircraft&quot;;\n\n@Component({\n  selector: &quot;app-aircraft&quot;,\n  imports: [FormField, ValidationErrorsPane],\n  templateUrl: &quot;.\/aircraft-form.html&quot;,\n})\nexport class AircraftForm {\n  aircraft = input.required&lt;FieldTree&lt;Aircraft&gt;&gt;();\n}<\/code><\/pre>\n<p>For arrays like the price 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 &quot;@angular\/core&quot;;\nimport { FieldTree, FormField } from &quot;@angular\/forms\/signals&quot;;\n\nimport { ValidationErrorsPane } \n  from &quot;..\/..\/..\/..\/shared\/ui-forms\/validation-errors\/validation-errors-pane&quot;;\nimport { Price } from &quot;..\/..\/..\/data\/price&quot;;\nimport { initPrice } from &quot;..\/..\/..\/data\/price-schema&quot;;\n\n@Component({\n  selector: &quot;app-prices&quot;,\n  imports: [FormField, ValidationErrorsPane],\n  templateUrl: &quot;.\/prices-form.html&quot;,\n})\nexport class PricesForm {\n  readonly prices = input.required&lt;FieldTree&lt;Price[]&gt;&gt;();\n\n  addPrice(): void {\n    const pricesForms = this.prices();\n    pricesForms().value.update((prices) =&gt; [...prices, { ...initPrice }]);\n  }\n}<\/code><\/pre>\n<h2>How Form Metadata Works? <span id=\"working-with-form-metadata\"><\/span><\/h2>\n<p>Instead of telling users after they entered something invalid, Signal Forms can provide metadata that tells them upfront what they are expected to provide:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.angulararchitects.io\/wp-content\/uploads\/2025\/11\/G3OgRm2WIAAeX0K.png\" alt=\"Field Metadata\" style=\"width:100%; max-width:600px\"><\/p>\n<p>For displaying the metadata, our example uses a component <code>app-field-meta-data-pane<\/code> that is imported into the flight form:<\/p>\n<pre><code class=\"language-typescript\">[...]\n\nimport { FieldMetaDataPane } from &#039;..\/..\/..\/shared\/ui-forms\/field-meta-data-pane\/field-meta-data-pane&#039;;\n\n[...]\n\n@Component({\n  selector: &#039;app-flight-edit&#039;,\n  imports: [[...], FieldMetaDataPane],\n  [...]\n})\nexport class FlightEdit {\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 Read Form Metadata? <span id=\"reading-meta-data\"><\/span><\/h3>\n<p>To read the metadata, call the <code>metadata<\/code> method with a key, e.g., <code>REQUIRED<\/code> or <code>MIN_LENGTH<\/code>. As we will see in a second, you can also define your own keys, such as <code>CITY<\/code> or <code>CITY2<\/code>. What you get back is either the metadata property itself or a Signal containing the property (more about this below), which you can display to the user:<\/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 &quot;@angular\/core&quot;;\nimport {\n  FieldTree,\n  REQUIRED,\n  MIN_LENGTH,\n  MAX_LENGTH,\n} from &quot;@angular\/forms\/signals&quot;;\n\nimport { CITY, CITY2 } from &quot;..\/..\/util-common\/properties&quot;;\n\n@Component({\n  selector: &quot;app-field-meta-data-pane&quot;,\n  imports: [],\n  templateUrl: &quot;.\/field-meta-data-pane.html&quot;,\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\n  protected readonly city = computed(() =&gt; this.fieldState().metadata(CITY));\n  protected readonly city2 = computed(() =&gt; this.fieldState().metadata(CITY2));\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;\n@if (city()) {\n&lt;span class=&quot;info info-small&quot;&gt;City&lt;\/span&gt;\n} @if (city2()) {\n&lt;span class=&quot;info info-small&quot;&gt;City&lt;\/span&gt;\n}<\/code><\/pre>\n<h3>How to Define Custom Form Metadata? <span id=\"defining-metadata\"><\/span><\/h3>\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, MetadataReducer } from &quot;@angular\/forms\/signals&quot;;\n\n\/\/\n\/\/  Property\n\/\/\nexport const CITY = createMetadataKey&lt;boolean&gt;();\n\n\/\/\n\/\/  AggregateProperty\n\/\/\nexport const CITY2 = createMetadataKey(MetadataReducer.or());<\/code><\/pre>\n<p>Now it gets a bit spicy: Let's imagine two validators define 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. However, if you define a reducer like <code>CITY2<\/code>, you can control how multiple values are combined.<\/p>\n<p>The reducer <code>MetadataReducer.or()<\/code> combines them via a logical OR (<code>value1 || value2 || value3<\/code>). Besides this, there is, for instance, <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 CITY3 = 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\nexport function validateCityHttp(schema: SchemaPath&lt;string&gt;) {\n  metadata(schema, CITY, () =&gt; true);\n\n  validateHttp(schema, { ... });\n}<\/code><\/pre>\n<h2>Null and Undefined Values <span id=\"null-and-undefined-values\"><\/span><\/h2>\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, Signal Forms's <code>form<\/code> function has no way of knowing that this field is supposed to exist in the form.<\/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: string;\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 Signal Form for this model in a component without a <code>delay<\/code>, Angular Signal forms cannot find the needed metadata for the <code>delay<\/code> field. From it's 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 different perspective from the domain perspective than from the form perspective. For this reason the Angular team recommends to distinguish between the two perspectives by defining two separate types:<\/p>\n<pre><code class=\"language-ts\">export interface FlightFormModel {\n  id: string;\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> you can use <code>null<\/code> as it has the semantic of 'empty value'. However, it would be even better to go with a sound default value. In our example, we can simply set the default delay to <code>0<\/code>. To bridge between the domain model and the form model, we can 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 could use 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 ? undefined : model.delay,\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 chapter xyz 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>Creating Custom Fields <span id=\"custom-fields\"><\/span><\/h2>\n<p>So far, we've only used the <code>FormField<\/code> directive with standard HTML tags. 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. Signal Forms makes the whole process much simpler: The widget implements the <code>FormValueControl&lt;T&gt;<\/code> interface, which just 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 &quot;@angular\/core&quot;;\nimport { FormValueControl, ValidationError } from &quot;@angular\/forms\/signals&quot;;\n\n@Component({\n  selector: &quot;app-delay-stepper&quot;,\n  imports: [],\n  templateUrl: &quot;.\/delay-stepper.html&quot;,\n  styleUrl: &quot;.\/delay-stepper.css&quot;,\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(&quot;DelayStepper, errors&quot;, 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 <code>errors<\/code>. This property is displayed with an effect for the sake of demonstration. 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;a (click)=&quot;inc()&quot;&gt;+15 Minutes&lt;\/a&gt; |\n  &lt;a (click)=&quot;dec()&quot;&gt;-15 Minutes&lt;\/a&gt;\n&lt;\/div&gt;\n}<\/code><\/pre>\n<h3>Note on Custom Checkboxes <span id=\"note-on-custom-checkboxes\"><\/span><\/h3>\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>Conclusion <span id=\"conclusion\"><\/span><\/h2>\n<p>Signal Forms show where Angular is heading: toward a form model that is fully reactive, strongly typed, and built around Signals. Instead of treating forms as a separate subsystem, they integrate form state, validation, and UI behavior into one coherent, composable API\u2014scaling from simple fields to nested object graphs and arrays.<\/p>\n<p>What stands out is the breadth of validation options (custom, conditional, multi-field, tree-based, and async\/HTTP) and how naturally these rules can be organized via schemas and reusable validator functions. Combined with metadata, forms can not only detect errors but also communicate expectations early, leading to a smoother user experience.<\/p>\n<p>Signal Forms are still experimental, but they are already worth exploring in real-world scenarios: start with a single feature form, extract reusable schemas, and incrementally add advanced validation where it delivers the most value. If you try them early and provide feedback, you can help shape a form API that many Angular apps will benefit from in the near future.<\/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 The long-awaited Signal Forms bridge a crucial gap between Angular&#8217;s Signal-based reactivity and user interaction. While currently [&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-02-14T12:24:37+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=\"36 minutes\" \/>\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-02-14T12:24:37+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/\"},\"wordCount\":3772,\"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-02-14T12:24:37+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-02-14T12:24:37+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":"36 minutes"},"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-02-14T12:24:37+00:00","mainEntityOfPage":{"@id":"https:\/\/www.angulararchitects.io\/en\/blog\/all-about-angulars-new-signal-forms\/"},"wordCount":3772,"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-02-14T12:24:37+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":32666,"href":"https:\/\/www.angulararchitects.io\/en\/wp-json\/wp\/v2\/posts\/31238\/revisions\/32666"}],"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}]}}