<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Brian Treese</title>
    <description>Angular tutorials, demos, and developer resources. Learn modern Angular techniques for components, signals, styling, animations, CDK, and more. Practical guides for Angular developers.</description>
    <link>https://briantree.se/</link>
    <atom:link href="https://briantree.se/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Fri, 10 Apr 2026 04:30:43 +0000</pubDate>
    <lastBuildDate>Fri, 10 Apr 2026 04:30:43 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator><item>
        <title>How to Get Specific Validation Errors with Angular Signal Forms</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;f you’ve ever tried to build something like a password checklist in Signal Forms, you’ve probably run into a frustrating limitation. You need to know if a specific validation rule failed, but the errors API doesn’t make that easy. And if you try to rely on error indexes, things can break pretty quickly as errors come and go. This post walks through how Angular v22 gives us a simple fix for this with the new &lt;code&gt;getError()&lt;/code&gt; function.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/gfYXk9p_PG4?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;How to Get Specific Validation Errors with Angular Signal Forms&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;a-password-checklist-with-angular-signal-forms&quot;&gt;A Password Checklist with Angular Signal Forms&lt;/h2&gt;

&lt;p&gt;Here we have a simple sign-up form with a username and password field:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-09/password-checklist-form.jpg&quot; alt=&quot;A sign-up form with a password checklist showing requirements for uppercase, number, special character, and length&quot; width=&quot;1370&quot; height=&quot;1088&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;For the password field, we have a list of requirements:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;One uppercase letter&lt;/li&gt;
  &lt;li&gt;One number&lt;/li&gt;
  &lt;li&gt;One special character&lt;/li&gt;
  &lt;li&gt;And at least 8 characters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we add each required piece to the password, the UI updates letting us know each requirement has been met:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-09/password-checklist-form-success.gif&quot; alt=&quot;A sign-up form with a password checklist showing requirements for uppercase, number, special character, and length&quot; width=&quot;1370&quot; height=&quot;934&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;We’re going to implement this specific functionality using the Signal Forms API.&lt;/p&gt;

&lt;p&gt;The tricky part here is that this UI isn’t just showing errors, we need to track each rule individually.&lt;/p&gt;

&lt;h2 id=&quot;why-error-indexes-break-in-angular-signal-forms&quot;&gt;Why Error Indexes Break in Angular Signal Forms&lt;/h2&gt;

&lt;p&gt;The validators on this form look like this:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;minLength&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Password must include at least one number&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;validate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  	&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\d&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  	  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;missingNumber&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
  	&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  	&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Password must include at least one uppercase letter&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;validate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;A-Z&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;missingUppercase&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Password must include at least one special character&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;validate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;[^&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;A-Za-z0-9&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;missingSpecialChar&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There are four validators on the password field:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The first is the &lt;a href=&quot;https://angular.dev/api/forms/signals/minLength?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;minLength validator&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The second is a &lt;a href=&quot;https://angular.dev/api/forms/signals/validate?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;custom validator&lt;/a&gt; that checks for a number&lt;/li&gt;
  &lt;li&gt;The third is a custom validator that checks for an uppercase letter&lt;/li&gt;
  &lt;li&gt;And the fourth is a custom validator that checks for a special character&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To make this concept work, we need to bind a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;valid&lt;/code&gt; class on each list item when its specific validation error does not exist.&lt;/p&gt;

&lt;p&gt;One way we might try to do this is by accessing the form field, then using the errors array to get the validator by index:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[class.valid]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;!form.password().errors()[2]&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    One uppercase letter
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We’ll access the password field from the form, then use the errors array to grab a validator by index.&lt;/p&gt;

&lt;p&gt;In this case, the minlength was first, the missing number was second, and this error was third&lt;/p&gt;

&lt;p&gt;Arrays are zero-based, so we’ll go with an index of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we save and look at this in the browser, already we can see we have a problem:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-09/error-index-bug.jpg&quot; alt=&quot;The minLength error is not showing up yet because the field is empty&quot; width=&quot;1468&quot; height=&quot;1054&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Notice the minLength error isn’t showing up yet.&lt;/p&gt;

&lt;p&gt;That’s because the field is currently empty, and minLength only triggers when there’s actually a value.&lt;/p&gt;

&lt;p&gt;This means that the index of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; that we used for our uppercase error is already incorrect.&lt;/p&gt;

&lt;p&gt;If I click into the password field and add an uppercase character, watch what happens:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-09/error-index-bug-2.jpg&quot; alt=&quot;Typing in the password field causes the error array order to change, breaking the UI logic&quot; width=&quot;1406&quot; height=&quot;1230&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The uppercase error is removed from the array, and the minLength error is added.&lt;/p&gt;

&lt;p&gt;The order of the array completely changes!&lt;/p&gt;

&lt;p&gt;The problem is that the errors array isn’t stable.&lt;/p&gt;

&lt;p&gt;It changes based on which validators are currently failing.&lt;/p&gt;

&lt;p&gt;So not only is this fragile, it’s fundamentally the wrong way to model this UI.&lt;/p&gt;

&lt;p&gt;But, luckily we’ve got more options!&lt;/p&gt;

&lt;h2 id=&quot;using-errorsfind-to-check-validation-rules&quot;&gt;Using errors().find() to Check Validation Rules&lt;/h2&gt;

&lt;p&gt;At this point, you might think “okay, I’ll just search the array.”&lt;/p&gt;

&lt;p&gt;Let’s switch to the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find&quot; target=&quot;_blank&quot;&gt;find()&lt;/a&gt; method instead where the kind equals &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;missingUppercase&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[class.valid]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;!form.password().errors().find(e =&amp;gt; e.kind === &apos;missingUppercase&apos;)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    One uppercase letter
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now this explicitly looks for the error with the kind we care about, regardless of its position in the array.&lt;/p&gt;

&lt;p&gt;After saving, if I click into the field and type an uppercase letter, you can see this worked correctly:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-09/find-success.jpg&quot; alt=&quot;Typing in the password field successfully checks off each requirement one by one&quot; width=&quot;1460&quot; height=&quot;724&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The uppercase letter requirement is properly colored and checked.&lt;/p&gt;

&lt;p&gt;So this works, but now we’re scanning the entire error array every time we want to check a single rule.&lt;/p&gt;

&lt;p&gt;It gets repetitive, and it doesn’t really match what we’re trying to do.&lt;/p&gt;

&lt;p&gt;Well, in Angular 22, there’s going to be an even better way!&lt;/p&gt;

&lt;h2 id=&quot;angular-22-using-geterror-for-clean-validation-checks&quot;&gt;Angular 22: Using getError() for Clean Validation Checks&lt;/h2&gt;

&lt;p&gt;Now, with reactive forms, we had a feature that allowed us to do this pretty easily.&lt;/p&gt;

&lt;p&gt;We would set this up using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hasError()&lt;/code&gt; function:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[class.valid]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;!signUpForm.controls.password.hasError(&apos;missingUppercase&apos;)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    One uppercase letter
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s pretty clean, readable, and it explicitly checks for the error we want.&lt;/p&gt;

&lt;p&gt;Well, in Angular 22, we’re getting something very similar for signal forms.&lt;/p&gt;

&lt;p&gt;Instead of scanning the entire array every time, Angular 22 introduces a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getError()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Now we can directly ask: “does this specific error exist?”&lt;/p&gt;

&lt;p&gt;Which is exactly what this UI needs.&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;ol&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[class.dirty]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.password().dirty()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[class.valid]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;!form.password().getError(&apos;missingUppercase&apos;)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    One uppercase letter
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[class.valid]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;!form.password().getError(&apos;missingNumber&apos;)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    One number
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[class.valid]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;!form.password().getError(&apos;missingSpecialChar&apos;)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    One special character
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[class.valid]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;!form.password().getError(&apos;minLength&apos;) &amp;amp;&amp;amp; form.password().value()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    At least 8 characters
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Much cleaner, right?&lt;/p&gt;

&lt;p&gt;One important detail, Signal Forms can have multiple errors with the same kind, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getError()&lt;/code&gt; will only return the first one.&lt;/p&gt;

&lt;p&gt;Also, for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;minLength&lt;/code&gt; requirement, this rule is a little different.&lt;/p&gt;

&lt;p&gt;We only want to evaluate it once the user has actually entered something.&lt;/p&gt;

&lt;p&gt;If we leave that off, we could add an 8-character value, then completely delete it, and the requirement would still show as satisfied.&lt;/p&gt;

&lt;h2 id=&quot;the-final-result&quot;&gt;The Final Result&lt;/h2&gt;

&lt;p&gt;Let’s try it out:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-09/geterror-success.gif&quot; alt=&quot;Typing in the password field successfully checks off each requirement one by one&quot; width=&quot;1460&quot; height=&quot;930&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And there we go! Each item is now properly checked off as each requirement is met.&lt;/p&gt;

&lt;p&gt;This is the key idea: instead of working with the entire error list, we’re working with individual validation states.&lt;/p&gt;

&lt;h2 id=&quot;why-geterror-makes-signal-forms-easier-to-use&quot;&gt;Why getError() Makes Signal Forms Easier to Use&lt;/h2&gt;

&lt;p&gt;So &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getError()&lt;/code&gt; brings Signal Forms much closer to the ergonomics we had with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hasError()&lt;/code&gt; in reactive forms, but with a more flexible error model.&lt;/p&gt;

&lt;p&gt;And when you’re building UIs like this, where each validation rule matters individually, it makes a big difference.&lt;/p&gt;

&lt;h2 id=&quot;get-ahead-of-angulars-next-shift&quot;&gt;Get Ahead of Angular’s Next Shift&lt;/h2&gt;

&lt;p&gt;Most Angular apps today still rely on the old reactive or template-driven forms, but that’s starting to shift.&lt;/p&gt;

&lt;p&gt;Signal Forms are new, and not widely adopted yet, which makes this a good time to get ahead of the curve.&lt;/p&gt;

&lt;p&gt;I created a course that walks through everything in a real-world context if you want to get up to speed early: 👉 &lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms: Build Modern Forms with Signals&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/fZZ1UVkyB4I?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular 22: Mix Signal Forms and Reactive Forms Seamlessly&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/Getting-Specific-Validation-Errors-with-Signal-Forms&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/angular/angular/commit/709f5a390ca0de04f8066012a5cb36999f2fd4a6&quot; target=&quot;_blank&quot;&gt;The commit that makes this all possible&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;My course “Angular Signal Forms: Build Modern Forms with Signals”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;f you’ve ever tried to build something like a password checklist in Signal Forms, you’ve probably run into a frustrating limitation. You need to know if a specific validation rule failed, but the errors API doesn’t make that easy. And if you try to rely on error indexes, things can break pretty quickly as errors come and go. This post walks through how Angular v22 gives us a simple fix for this with the new &lt;code&gt;getError()&lt;/code&gt; function.&lt;/p&gt;

</description><pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-targeted-validation-errors-signal-forms/</link>
        <guid isPermaLink="true">https://briantree.se/angular-targeted-validation-errors-signal-forms/</guid><category>Angular</category><category>Angular v22</category><category>Signal Forms</category><category>Angular Forms</category><category>Form Validation</category></item><item>
        <title>Angular 22&apos;s New Built-in Debounce for Async Validation Explained</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;f you&apos;re using &lt;a href=&quot;https://angular.dev/essentials/signal-forms?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;Signal Forms&lt;/a&gt; with async validation, you&apos;ve probably run into a frustrating issue. You either debounce every validator with the &lt;a href=&quot;https://angular.dev/api/forms/signals/debounce?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;debounce()&lt;/a&gt; function, or you end up hitting your API on every keystroke. Neither is great, but Angular 22 fixes this in a really clean way. This post walks through how the new &lt;a href=&quot;https://github.com/angular/angular/commit/24e52d450d201e3da90bb64f84358f9eccd7877d&quot; target=&quot;_blank&quot;&gt;built-in debounce&lt;/a&gt; works and why it makes Signal Forms even better.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/4ynDt0-Cj7A?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular 22&amp;#39;s New Built-in Debounce for Async Validation Explained&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;the-problem-debouncing-delays-all-validators&quot;&gt;The Problem: Debouncing Delays All Validators&lt;/h2&gt;

&lt;p&gt;When building forms with async validation, we want to wait for the user to stop typing before hitting the API.&lt;/p&gt;

&lt;p&gt;Here we can type really slowly without triggering any validation or pending messages while validators are running:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-02/typing-slowly.gif&quot; alt=&quot;Typing slowly in the username field without triggering validation&quot; width=&quot;1370&quot; height=&quot;906&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;We’re waiting for the user to stop typing before we run our validation.&lt;/p&gt;

&lt;p&gt;Once we stop, the validator fires and shows us a pending message:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-02/pending-message.jpg&quot; alt=&quot;Pending message showing the username is being validated&quot; width=&quot;1040&quot; height=&quot;382&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;But in this case, the username “test” already exists, so now we see our error message:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-02/validation-error.jpg&quot; alt=&quot;Validation error showing the username already exists&quot; width=&quot;1016&quot; height=&quot;367&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The email field works the exact same way:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-02/email-validation-error.gif&quot; alt=&quot;An email field using validateHttp() and debounce()&quot; width=&quot;1432&quot; height=&quot;590&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;We get a pending message while validation is running, followed by an error message if the email is registered.&lt;/p&gt;

&lt;h2 id=&quot;the-old-way-field-level-debounce&quot;&gt;The Old Way: Field-Level Debounce&lt;/h2&gt;

&lt;p&gt;Here is how this form is currently wired up using Angular’s Signal Forms API.&lt;/p&gt;

&lt;p&gt;We have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model&lt;/code&gt; signal holding the state for our sign up form, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; declaration.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SignUpForm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;A username is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;An email address is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Please enter a valid email address&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;validateAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;debounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But here’s the catch, this standalone debounce function applies to the entire field.&lt;/p&gt;

&lt;p&gt;That means it debounces all validators, even synchronous ones like &lt;a href=&quot;https://angular.dev/api/forms/signals/required?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;required()&lt;/a&gt; or &lt;a href=&quot;https://angular.dev/api/forms/signals/minLength?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;minLength()&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And this is an issue because it can delay instant feedback for simple checks just to accommodate our async call.&lt;/p&gt;

&lt;p&gt;The same applies to &lt;a href=&quot;https://angular.dev/api/forms/signals/validateHttp?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;validateHttp()&lt;/a&gt; on the email field:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;validateHttp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;debounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We define our request and handlers, and then we have to tack on the debounce function at the end.&lt;/p&gt;

&lt;h2 id=&quot;the-new-way-validator-level-debounce&quot;&gt;The New Way: Validator-Level Debounce&lt;/h2&gt;

&lt;p&gt;With Angular 22, we have a better approach available.&lt;/p&gt;

&lt;p&gt;The Angular team added a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounce&lt;/code&gt; option directly into the validation functions.&lt;/p&gt;

&lt;h3 id=&quot;angular-22-fix-built-in-debounce-for-async-validators&quot;&gt;Angular 22 Fix: Built-in Debounce for Async Validators&lt;/h3&gt;

&lt;p&gt;So, we can delete the old &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounce&lt;/code&gt; function call from and instead, inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;validateAsync()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;validateHttp()&lt;/code&gt;, we can just add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounce&lt;/code&gt; property directly:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;validateAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;debounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// debounce(s.username, 2000);&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;validateHttp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;debounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// debounce(s.email, 2000);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it!&lt;/p&gt;

&lt;p&gt;Before, debounce was applied at the field level.&lt;/p&gt;

&lt;p&gt;Now it’s applied at the validator level.&lt;/p&gt;

&lt;p&gt;This is important because async validation is fundamentally different from synchronous validation.&lt;/p&gt;

&lt;p&gt;It’s network-bound, not CPU-bound.&lt;/p&gt;

&lt;p&gt;So it should be controlled independently.&lt;/p&gt;

&lt;h2 id=&quot;the-final-result&quot;&gt;The Final Result&lt;/h2&gt;

&lt;p&gt;Now, synchronous validators can fire instantly, but our async check waits for the debounce just like the original example:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/04-02/final-result.gif&quot; alt=&quot;Typing in the username field with debounced async validation with the new debounce option&quot; width=&quot;1294&quot; height=&quot;404&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;We see the pending message just like we used to, and then we get our validation error message.&lt;/p&gt;

&lt;p&gt;Our async logic works the same, but we’re no longer holding up the rest of the validators tied to this control.&lt;/p&gt;

&lt;h2 id=&quot;why-this-makes-signal-forms-better&quot;&gt;Why This Makes Signal Forms Better&lt;/h2&gt;

&lt;p&gt;So the key shift here is simple, debounce is no longer a field-level concern, it’s a validator-level concern.&lt;/p&gt;

&lt;p&gt;That means better UX, cleaner code, and no more tradeoffs between responsiveness and API calls.&lt;/p&gt;

&lt;p&gt;If you’re building complex forms in large enterprise apps, this small change reduces boilerplate and keeps your validation logic co-located with the validator where it belongs.&lt;/p&gt;

&lt;h2 id=&quot;get-ahead-of-angulars-next-shift&quot;&gt;Get Ahead of Angular’s Next Shift&lt;/h2&gt;

&lt;p&gt;Most Angular apps today still rely on the old reactive or template-driven forms, but that’s starting to shift.&lt;/p&gt;

&lt;p&gt;Signal Forms are new, and not widely adopted yet, which makes this a good time to get ahead of the curve.&lt;/p&gt;

&lt;p&gt;I created a course that walks through everything in a real-world context if you want to get up to speed early: 👉 &lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms: Build Modern Forms with Signals&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/fZZ1UVkyB4I?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular 22: Mix Signal Forms and Reactive Forms Seamlessly&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-debounce-async-validation&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/angular/angular/commit/24e52d450d201e3da90bb64f84358f9eccd7877d&quot; target=&quot;_blank&quot;&gt;The commit that makes this all possible&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;My course “Angular Signal Forms: Build Modern Forms with Signals”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;f you&apos;re using &lt;a href=&quot;https://angular.dev/essentials/signal-forms?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;Signal Forms&lt;/a&gt; with async validation, you&apos;ve probably run into a frustrating issue. You either debounce every validator with the &lt;a href=&quot;https://angular.dev/api/forms/signals/debounce?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco&quot; target=&quot;_blank&quot;&gt;debounce()&lt;/a&gt; function, or you end up hitting your API on every keystroke. Neither is great, but Angular 22 fixes this in a really clean way. This post walks through how the new &lt;a href=&quot;https://github.com/angular/angular/commit/24e52d450d201e3da90bb64f84358f9eccd7877d&quot; target=&quot;_blank&quot;&gt;built-in debounce&lt;/a&gt; works and why it makes Signal Forms even better.&lt;/p&gt;

</description><pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-signal-forms-debounce-async-validation/</link>
        <guid isPermaLink="true">https://briantree.se/angular-signal-forms-debounce-async-validation/</guid><category>Angular</category><category>Angular v22</category><category>Signal Forms</category><category>Angular Forms</category><category>Forms</category></item><item>
        <title>Angular 22: Mix Signal Forms and Reactive Forms Seamlessly</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;W&lt;/span&gt;hat if you could start using Signal Forms today without touching your existing &lt;a href=&quot;https://angular.dev/guide/forms/reactive-forms&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Reactive&lt;/a&gt; or &lt;a href=&quot;https://angular.dev/guide/forms/template-driven-forms&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Template-driven&lt;/a&gt; forms at all? In Angular 22, you&apos;ll be able to build Signal-based custom form controls that drop right into your existing forms with no massive rewrites required. This post walks through how to migrate a custom control from &lt;a href=&quot;https://angular.dev/api/forms/ControlValueAccessor&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ControlValueAccessor&lt;/a&gt; to &lt;a href=&quot;https://angular.dev/api/forms/signals/FormValueControl&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;FormValueControl&lt;/a&gt; while keeping the parent form completely intact.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/RQUFjZdFqGE?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular 22: Mix Signal Forms and Reactive Forms Seamlessly&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;reactive-forms-setup-with-a-custom-control&quot;&gt;Reactive Forms Setup with a Custom Control&lt;/h2&gt;

&lt;p&gt;Here, we have a simple cart form with a quantity control, a coupon code, an email field, and a gift wrap checkbox.&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-26/cart-form-demo.jpg&quot; alt=&quot;A cart form with a quantity stepper, coupon code, email, and gift wrap checkbox&quot; width=&quot;998&quot; height=&quot;1058&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;This form is currently built using standard Reactive Forms.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support/tree/main/src/app/quantity-stepper&quot; target=&quot;_blank&quot;&gt;quantity control&lt;/a&gt; is actually a custom form control built using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ControlValueAccessor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we click the plus and minus buttons, the value of our form updates correctly:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-26/custom-control-updating-value.jpg&quot; alt=&quot;Clicking the plus and minus buttons updates the quantity value&quot; width=&quot;1080&quot; height=&quot;544&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And if we go under 1 item, it triggers our validation and we see the error message appear:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-26/cart-form-validation.jpg&quot; alt=&quot;Changing the quantity below 1 triggers a validation error message&quot; width=&quot;1050&quot; height=&quot;400&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Everything works, but the underlying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ControlValueAccessor&lt;/code&gt; implementation is incredibly verbose.&lt;/p&gt;

&lt;p&gt;Let’s look at the code so you can see what I mean.&lt;/p&gt;

&lt;h2 id=&quot;the-old-way-how-controlvalueaccessor-works-in-angular&quot;&gt;The Old Way: How ControlValueAccessor Works in Angular&lt;/h2&gt;

&lt;p&gt;Let’s start with the code for &lt;a href=&quot;https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support/tree/main/src/app/cart&quot; target=&quot;_blank&quot;&gt;the parent form&lt;/a&gt; component.&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support/blob/main/src/app/cart/cart.component.html&quot; target=&quot;_blank&quot;&gt;the template&lt;/a&gt;, we have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formGroup&lt;/code&gt; directive that wraps all of our form controls:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;cart-form&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;novalidate&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formGroup]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Within this form group, our custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app-quantity-stepper&lt;/code&gt; component is wired up using the standard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formControlName&lt;/code&gt; directive as well:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;app-quantity-stepper&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;qty&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;formControlName=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;quantity&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;All the other fieds use the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formControlName&lt;/code&gt; directive too.&lt;/p&gt;

&lt;p&gt;Now let’s switch and look at &lt;a href=&quot;https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support/blob/main/src/app/cart/cart.component.ts&quot; target=&quot;_blank&quot;&gt;the component TypeScript&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, we have the interface for our form, strongly typing everything with &lt;a href=&quot;https://angular.dev/api/forms/FormControl&quot; target=&quot;_blank&quot;&gt;FormControls&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;CartForm&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;couponCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;giftWrap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we have the &lt;a href=&quot;https://angular.dev/api/forms/FormGroup&quot; target=&quot;_blank&quot;&gt;FormGroup&lt;/a&gt; itself:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ReactiveFormsModule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Validators&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;CartForm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;quantity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;couponCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;giftWrap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The quantity form control is initialized to 1 and set as non-nullable with a minimum value validator of 1.&lt;/p&gt;

&lt;p&gt;That’s why, once we went below a quantity of one, we saw our validation error.&lt;/p&gt;

&lt;p&gt;This is a pretty standard Reactive Forms setup.&lt;/p&gt;

&lt;p&gt;The complexity is actually hiding inside that custom stepper component.&lt;/p&gt;

&lt;h2 id=&quot;why-controlvalueaccessor-is-so-verbose&quot;&gt;Why ControlValueAccessor Is So Verbose&lt;/h2&gt;

&lt;p&gt;First, we have this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;providers&lt;/code&gt; array:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;forwardRef&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;NG_VALUE_ACCESSOR&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;nl&quot;&gt;providers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;provide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;NG_VALUE_ACCESSOR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;useExisting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;forwardRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;QuantityStepperComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;multi&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We have to provide &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NG_VALUE_ACCESSOR&lt;/code&gt; and use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forwardRef&lt;/code&gt; just to tell Angular that this component can act as a form control.&lt;/p&gt;

&lt;p&gt;Our class then implements the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ControlValueAccessor&lt;/code&gt; interface:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ControlValueAccessor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;QuantityStepperComponent&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ControlValueAccessor&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Inside the class we have a private &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; signal to hold the state, a public &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; property to expose it for use in the form, and an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isDisabled&lt;/code&gt; Boolean property too:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;isDisabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After this, we implement empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onChange&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onTouched&lt;/code&gt; callbacks, and wire up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;writeValue&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;registerOnChange&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;registerOnTouched&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setDisabledState&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onTouched&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{};&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;writeValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;registerOnChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onChange&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; 
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;registerOnTouched&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onTouched&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; 
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;setDisabledState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isDisabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; 
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Pretty much none of this is your actual business logic.&lt;/p&gt;

&lt;p&gt;It’s mostly all just needed due to the fact that this is a custom control built with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ControlValueAccessor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;increment&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;decrement&lt;/code&gt; functions, we update our internal signal.&lt;/p&gt;

&lt;p&gt;But we also have to manually call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onChange&lt;/code&gt; so the parent form knows about it:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;decrement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; 
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s a lot of stuff, right?&lt;/p&gt;

&lt;p&gt;And this is a fairly simple example, I’ve certainly seen much more complex custom controls in my experience working with Angular.&lt;/p&gt;

&lt;h2 id=&quot;replace-controlvalueaccessor-with-formvaluecontrol&quot;&gt;Replace ControlValueAccessor with FormValueControl&lt;/h2&gt;

&lt;p&gt;Now, imagine we are tasked with updating this quantity stepper component to use Signal Forms.&lt;/p&gt;

&lt;p&gt;We don’t want to rewrite the parent cart component yet, because maybe it’s a massive, complicated form.&lt;/p&gt;

&lt;p&gt;Well, with the upcoming release of Angular v22, we will be able to do exactly that!&lt;/p&gt;

&lt;p&gt;With Signal Forms, migrating this component is incredibly easy.&lt;/p&gt;

&lt;p&gt;We can completely delete the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;providers&lt;/code&gt; array, the private &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; signal, and all of those &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ControlValueAccessor&lt;/code&gt; methods.&lt;/p&gt;

&lt;p&gt;Instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ControlValueAccessor&lt;/code&gt;, we’ll implement the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormValueControl&lt;/code&gt; interface, and we’ll type it to a number.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormValueControl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms/signals&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-quantity-stepper&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;templateUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./quantity-stepper.component.html&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;styleUrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./quantity-stepper.component.scss&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;changeDetection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;OnPush&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;QuantityStepperComponent&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormValueControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;isDisabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;decrement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; 
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; 
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When you implement this interface, instead of requiring a bunch of methods, Angular now expects a single “value” &lt;a href=&quot;https://angular.dev/api/core/model&quot; target=&quot;_blank&quot;&gt;model()&lt;/a&gt; signal for the value of the control.&lt;/p&gt;

&lt;p&gt;We also changed our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isDisabled&lt;/code&gt; property to an &lt;a href=&quot;https://angular.dev/api/core/input&quot; target=&quot;_blank&quot;&gt;input&lt;/a&gt; initialized to false.&lt;/p&gt;

&lt;p&gt;We don’t need to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onChange&lt;/code&gt; anymore, so all we need to do now is update the signal value in our increment and decrement functions.&lt;/p&gt;

&lt;p&gt;That’s the entire component class now!&lt;/p&gt;

&lt;p&gt;Next, we need to switch over to the parent cart component and make some massive, complicated changes to the parent form so it can talk to this new signal-based control…&lt;/p&gt;

&lt;p&gt;Just kidding!&lt;/p&gt;

&lt;p&gt;Yeah, we’re not doing that.&lt;/p&gt;

&lt;p&gt;In Angular 22, we won’t have to make any changes to intermix custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormValueControls&lt;/code&gt; with classic Reactive Forms or Template-Driven Forms.&lt;/p&gt;

&lt;p&gt;Let’s save and test it out!&lt;/p&gt;

&lt;h2 id=&quot;the-final-result&quot;&gt;The Final Result&lt;/h2&gt;

&lt;p&gt;To start, our form looks exactly the same, so that’s good:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-26/signal-form-control-working.jpg&quot; alt=&quot;The updated form still works perfectly with the new Signal-based child control&quot; width=&quot;1006&quot; height=&quot;1068&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And after adjusting the quantity, the value of our Reactive Form is updating correctly in real time, now driven by our Signal-based child control:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-26/signal-form-control-updating-value.jpg&quot; alt=&quot;The value of our Reactive Form is updating correctly in real time, now driven by our Signal-based child control&quot; width=&quot;1032&quot; height=&quot;530&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Then if I go under 1 item again, it triggers the Reactive Forms validation, and our error message still works perfectly:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-26/signal-form-control-validation-error.jpg&quot; alt=&quot;The error message appearing when the quantity is below 1&quot; width=&quot;1010&quot; height=&quot;422&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;So, this is still a Reactive Form, but the control itself is now fully signal-based.&lt;/p&gt;

&lt;h2 id=&quot;key-takeaway-migrate-to-signal-forms-without-rewriting-everything&quot;&gt;Key Takeaway: Migrate to Signal Forms Without Rewriting Everything&lt;/h2&gt;

&lt;p&gt;Angular 22 allows Signal-based custom controls to work seamlessly with existing Reactive and Template-Driven Forms, no parent form changes required.&lt;/p&gt;

&lt;p&gt;We simply continue to use our existing parent form with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt;, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formControlName&lt;/code&gt; directive, and it just works.&lt;/p&gt;

&lt;p&gt;Angular automatically bridges Signal Forms and Reactive Forms for you, meaning you can modernize your large applications one component at a time!&lt;/p&gt;

&lt;h2 id=&quot;get-ahead-of-angulars-next-shift&quot;&gt;Get Ahead of Angular’s Next Shift&lt;/h2&gt;

&lt;p&gt;Most Angular apps today still rely on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ControlValueAccessor&lt;/code&gt; for custom form controls, but that’s starting to shift.&lt;/p&gt;

&lt;p&gt;Signal Forms are new, and not widely adopted yet, which makes this a good time to get ahead of the curve.&lt;/p&gt;

&lt;p&gt;I created a course that walks through everything in a real-world context if you want to get up to speed early: 👉 &lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms: Build Modern Forms with Signals&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/fZZ1UVkyB4I?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular 22: Mix Signal Forms and Reactive Forms Seamlessly&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/angular/angular/commit/c4ce3f345fdb14595f0991dff488c4043a0fc71c&quot; target=&quot;_blank&quot;&gt;The commit that makes this all possible&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://angular.dev/guide/forms/signals/custom-controls&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms Custom Controls Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;My course “Angular Signal Forms: Build Modern Forms with Signals”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;W&lt;/span&gt;hat if you could start using Signal Forms today without touching your existing &lt;a href=&quot;https://angular.dev/guide/forms/reactive-forms&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Reactive&lt;/a&gt; or &lt;a href=&quot;https://angular.dev/guide/forms/template-driven-forms&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Template-driven&lt;/a&gt; forms at all? In Angular 22, you&apos;ll be able to build Signal-based custom form controls that drop right into your existing forms with no massive rewrites required. This post walks through how to migrate a custom control from &lt;a href=&quot;https://angular.dev/api/forms/ControlValueAccessor&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ControlValueAccessor&lt;/a&gt; to &lt;a href=&quot;https://angular.dev/api/forms/signals/FormValueControl&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;FormValueControl&lt;/a&gt; while keeping the parent form completely intact.&lt;/p&gt;

</description><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/template-and-reactive-forms-form-value-control-support/</link>
        <guid isPermaLink="true">https://briantree.se/template-and-reactive-forms-form-value-control-support/</guid><category>Angular</category><category>Angular v22</category><category>Angular Forms</category><category>Signal Forms</category><category>Reactive Forms</category></item><item>
        <title>Angular’s New debounced() Signal Explained</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;E&lt;/span&gt;very Angular developer has faced it, an input that spams the backend with every single keystroke. The classic solution involves pulling in RxJS and using &lt;a href=&quot;https://rxjs.dev/api/operators/debounceTime&quot; target=&quot;_blank&quot;&gt;debounceTime&lt;/a&gt;, but it requires converting signals to observables and thinking in streams. As of Angular v22, there’s a new, cleaner way. The new experimental &lt;a href=&quot;https://github.com/angular/angular/commit/b918beda323eefef17bf1de03fde3d402a3d4af0&quot; target=&quot;_blank&quot;&gt;debounced()&lt;/a&gt; signal primitive lets you solve this problem in a more declarative, signal-native way. This post walks through the old way and then refactors it to the new, showing you exactly how to simplify your async data-fetching logic.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/jKffTaEL1JI?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular’s New debounced() Signal Explained&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;the-problem-too-many-api-requests&quot;&gt;The Problem: Too Many API Requests&lt;/h2&gt;

&lt;p&gt;Let’s start with a simple product search app:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-19/simple-product-search-app.jpg&quot; alt=&quot;A simple product search app&quot; width=&quot;1382&quot; height=&quot;682&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;It looks fine on the surface, but the real story is in the Network tab:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-19/network-spam.gif&quot; alt=&quot;A GIF showing the browser&apos;s network tab firing a new API request on every keystroke in the search input&quot; width=&quot;1908&quot; height=&quot;944&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;As you type a search term you can see a new HTTP request firing for each character typed.&lt;/p&gt;

&lt;p&gt;In a real-world application, this is a ton of unnecessary load on your backend and can create a jumpy, unpleasant user experience.&lt;/p&gt;

&lt;p&gt;This is the classic problem we need to solve.&lt;/p&gt;

&lt;h2 id=&quot;the-old-way-debouncing-with-rxjs-debouncetime&quot;&gt;The Old Way: Debouncing with RxJS &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounceTime&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Our initial component uses a mix of signals and RxJS.&lt;/p&gt;

&lt;p&gt;We have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;query&lt;/code&gt; signal that holds the search term, which is converted to an observable using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toObservable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;products&lt;/code&gt; are then loaded inside a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toSignal&lt;/code&gt; block that pipes the query observable through several RxJS operators:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;http&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;inject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;HttpClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;$query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;toObservable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;toSignal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;distinctUntilChanged&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;switchMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* ... */&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})),&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;startWith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;loading&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;catchError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}))&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;idle&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;initialValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;idle&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The traditional fix is to add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounceTime&lt;/code&gt; operator to the pipe.&lt;/p&gt;

&lt;p&gt;It’s a one-line change that tells RxJS to wait for a pause in emissions (e.g., 1000ms) before letting the value proceed:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;$query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;debounceTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Wait for 1 second of silence&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;distinctUntilChanged&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;switchMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;cm&quot;&gt;/* ... */&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This works perfectly:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-19/network-spam-fixed-with-rxjs-debounce-time.gif&quot; alt=&quot;A GIF showing the browser&apos;s network tab firing a new API request after the user stops typing for 1 second&quot; width=&quot;1918&quot; height=&quot;904&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The network spam stops, and only one request is sent after the user stops typing.&lt;/p&gt;

&lt;p&gt;But it forces us into the RxJS world of observables and pipes, even if the rest of our app is signal-first.&lt;/p&gt;

&lt;p&gt;What if we could stay in the world of signals?&lt;/p&gt;

&lt;p&gt;Well, as of Angular v22, we will be able to!&lt;/p&gt;

&lt;h2 id=&quot;the-new-way-debounced-and-resource-in-angular-v22&quot;&gt;The New Way: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounced()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource()&lt;/code&gt; in Angular v22&lt;/h2&gt;

&lt;p&gt;The Angular team has introduced a new experimental primitive, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounced&lt;/code&gt;, and it can work together with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource&lt;/code&gt; to solve this exact problem elegantly.&lt;/p&gt;

&lt;h3 id=&quot;step-1-create-a-debounced-signal&quot;&gt;Step 1: Create a Debounced Signal&lt;/h3&gt;

&lt;p&gt;First, we’ll create a new signal that is a debounced version of our original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;query&lt;/code&gt; signal.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounced()&lt;/code&gt; function from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@angular/core&lt;/code&gt; makes this trivial.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;debounced&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;debouncedQuery&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;debounced&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debouncedQuery&lt;/code&gt; is now a read-only signal that will only update its value when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;query&lt;/code&gt; signal has been stable for 1000 milliseconds.&lt;/p&gt;

&lt;h3 id=&quot;step-2-refactor-to-use-resource&quot;&gt;Step 2: Refactor to Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource()&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Next, we’ll completely replace our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toSignal&lt;/code&gt; implementation with the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource()&lt;/code&gt; primitive.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource&lt;/code&gt; is purpose-built for loading asynchronous data from a signal.&lt;/p&gt;

&lt;p&gt;We can delete the entire &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;products&lt;/code&gt; signal and its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toSignal&lt;/code&gt; block and replace it with this:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;debouncedQuery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;loader&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class=&quot;nx&quot;&gt;firstValueFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* ... */&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s break this down:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt;&lt;/strong&gt;: A function that returns the current search query from the debounced signal (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.debouncedQuery.value()&lt;/code&gt;), or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;undefined&lt;/code&gt; if the query is empty. When this value changes, the resource automatically re-fetches.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loader&lt;/code&gt;&lt;/strong&gt;: A function that receives the resolved &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; and fetches data using Angular’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpClient&lt;/code&gt;. Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpClient&lt;/code&gt; returns an Observable, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;firstValueFrom()&lt;/code&gt; is used to convert it to a Promise. The result is then unwrapped to return just the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;products&lt;/code&gt; array.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource&lt;/code&gt; primitive automatically manages the loading, error, and data states for us based on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; signal and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loader&lt;/code&gt; function’s execution.&lt;/p&gt;

&lt;h2 id=&quot;updating-the-template-for-the-resource-api&quot;&gt;Updating the Template for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource&lt;/code&gt; API&lt;/h2&gt;

&lt;p&gt;The new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource&lt;/code&gt; primitive has a different template API than our old status-based object.&lt;/p&gt;

&lt;p&gt;Instead of checking a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; property, we use methods like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isLoading()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our old &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@switch&lt;/code&gt; block gets replaced with a set of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@if&lt;/code&gt; conditions:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@if (query()) {
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- If there&apos;s a search query --&amp;gt;&lt;/span&gt;
  @if (products.isLoading()) {
    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Show loading spinner --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;state loading&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;spinner&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Fetching products…&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  } @else {
    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Show results --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;results-list&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      @for (product of products.value(); track product) {
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;result-item&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&amp;lt;strong&amp;gt;&lt;/span&gt;{{ product.title }}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;{{ product.price | currency }}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      }
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  }
} @else {
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- If there&apos;s no query, show idle state --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;state idle&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    Start typing to search products
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;We first check if the base &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;query()&lt;/code&gt; signal has a value. If not, we show the idle message.&lt;/li&gt;
  &lt;li&gt;If it does, we then check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;products.isLoading()&lt;/code&gt;. If true, we show the spinner.&lt;/li&gt;
  &lt;li&gt;Finally, if it’s not loading, we can safely access the data via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;products.value()&lt;/code&gt; and render the results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-final-result&quot;&gt;The Final Result&lt;/h2&gt;

&lt;p&gt;With these changes, the application behaves identically to the optimized RxJS version:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-19/network-spam-fixed-with-debounced-signal.gif&quot; alt=&quot;A GIF showing the browser&apos;s network tab firing a new API request after the user stops typing for 1 second&quot; width=&quot;1918&quot; height=&quot;920&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Typing in the search box only fires a single API request after the user has stopped typing for a second.&lt;/p&gt;

&lt;p&gt;The difference is that our component logic is now almost 100% signal-based.&lt;/p&gt;

&lt;p&gt;No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toObservable&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pipe()&lt;/code&gt;, no manual subscriptions.&lt;/p&gt;

&lt;p&gt;This is a huge step forward for reactivity in Angular, giving us a more declarative, signal-native way to handle one of the most common patterns in web development.&lt;/p&gt;

&lt;h2 id=&quot;get-ahead-of-angulars-nextshift&quot;&gt;Get Ahead of Angular’s Next Shift&lt;/h2&gt;

&lt;p&gt;Most Angular apps today still rely on reactive forms, but that’s starting to shift.&lt;/p&gt;

&lt;p&gt;Signal Forms are new, and not widely adopted yet, which makes this a good time to get ahead of the curve.&lt;/p&gt;

&lt;p&gt;I created a course that walks through everything in a real-world context if you want to get up to speed early: 👉 &lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/fZZ1UVkyB4I?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Signal Forms Course Preview&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/angular-v22-debounced-signals-example&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://angular.dev/guide/signals/resource&quot; target=&quot;_blank&quot;&gt;Angular Resource API&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://rxjs.dev/api/operators/debounceTime&quot; target=&quot;_blank&quot;&gt;RxJS debounceTime Docs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;My course “Angular Signal Forms: Build Modern Forms with Signals”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;E&lt;/span&gt;very Angular developer has faced it, an input that spams the backend with every single keystroke. The classic solution involves pulling in RxJS and using &lt;a href=&quot;https://rxjs.dev/api/operators/debounceTime&quot; target=&quot;_blank&quot;&gt;debounceTime&lt;/a&gt;, but it requires converting signals to observables and thinking in streams. As of Angular v22, there’s a new, cleaner way. The new experimental &lt;a href=&quot;https://github.com/angular/angular/commit/b918beda323eefef17bf1de03fde3d402a3d4af0&quot; target=&quot;_blank&quot;&gt;debounced()&lt;/a&gt; signal primitive lets you solve this problem in a more declarative, signal-native way. This post walks through the old way and then refactors it to the new, showing you exactly how to simplify your async data-fetching logic.&lt;/p&gt;

</description><pubDate>Thu, 19 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-debounced-signals-explained/</link>
        <guid isPermaLink="true">https://briantree.se/angular-debounced-signals-explained/</guid><category>Angular</category><category>Angular v22</category><category>Angular Signals</category><category>debounced</category><category>resource</category><category>TypeScript</category></item><item>
        <title>Angular Signal Forms: Is FormValueControl Better for Large Forms?</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;n a &lt;a href=&quot;/angular-signal-forms-structuring-large-forms/&quot;&gt;recent guide&lt;/a&gt; I showed a pattern for building large &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; using reusable form sections. But a common follow-up question kept coming up: &lt;em&gt;Why not just use &lt;a href=&quot;https://angular.dev/api/forms/signals/FormValueControl&quot; target=&quot;_blank&quot;&gt;FormValueControl&lt;/a&gt; instead?&lt;/em&gt; It sounded like a great idea, so I tried it. In this post you&apos;ll see how it works and why I&apos;m not completely convinced it&apos;s actually the better approach for this scenario.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/FN0PcwGa7ts?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular Signal Forms: Is FormValueControl Better for Large Forms?&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;the-original-approach&quot;&gt;The Original Approach&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/angular-signal-forms-structuring-large-forms/&quot;&gt;Structuring Large Forms&lt;/a&gt; - The field tree approach this post compares against&lt;/p&gt;

&lt;h2 id=&quot;angular-signal-forms-demo-reusable-form-sections&quot;&gt;Angular Signal Forms Demo: Reusable Form Sections&lt;/h2&gt;

&lt;p&gt;Here’s the form we’re working with:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/profile-form-original.jpg&quot; alt=&quot;The profile form with the account information, shipping address, and preferences sections&quot; width=&quot;1524&quot; height=&quot;1406&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;At the top we have an &lt;strong&gt;Account Information&lt;/strong&gt; section, below that is a &lt;strong&gt;Shipping Address&lt;/strong&gt; section, and finally there’s a &lt;strong&gt;Preferences&lt;/strong&gt; section at the bottom.&lt;/p&gt;

&lt;p&gt;The important thing to note is that each of these form sections is its own Angular component.&lt;/p&gt;

&lt;p&gt;This makes large forms easier to maintain because each section owns its own UI and logic.&lt;/p&gt;

&lt;p&gt;If you’ve ever worked on a giant form component with 300 lines of inputs, you know why this matters.&lt;/p&gt;

&lt;p&gt;It also makes these form sections reusable elsewhere in the app as needed.&lt;/p&gt;

&lt;p&gt;We also have a debug panel showing the real-time value and status of the form:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/debug-panel-original.jpg&quot; alt=&quot;The debug panel showing the real-time value and status of the form&quot; width=&quot;1050&quot; height=&quot;1056&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Typing in the first name updates the form value immediately:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/first-name-input-original.jpg&quot; alt=&quot;The first name input field with the value updating in real time&quot; width=&quot;1044&quot; height=&quot;544&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Now, let’s look at how the original implementation worked.&lt;/p&gt;

&lt;h2 id=&quot;building-reusable-angular-signal-forms-with-field-tree&quot;&gt;Building Reusable Angular Signal Forms with Field Tree&lt;/h2&gt;

&lt;p&gt;In &lt;a href=&quot;https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/blob/master/src/app/sign-up/profile-form/profile-form.component.html&quot; target=&quot;_blank&quot;&gt;the template&lt;/a&gt; for the profile form component, the parent that wires the separate form sections into a single form, we see the three section components:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(submit)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;onSubmit($event)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-account-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[form]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.account&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-address-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[form]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.shippingAddress&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-preferences-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[form]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.preferences&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each component receives a slice of the form’s field tree through an input called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The parent form owns the entire form structure and passes individual sections down to each component.&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/blob/master/src/app/sign-up/profile-form/profile-form.component.ts&quot; target=&quot;_blank&quot;&gt;the component TypeScript&lt;/a&gt;, we first define the interface for our profile form:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../account/account-form/account-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../shipping/address-form/address-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Preferences&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../account/preferences-form/preferences-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Profile&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each section is typed with an interface exported from the individual components themselves.&lt;/p&gt;

&lt;p&gt;Below this, we have our form model signal that holds the state of the entire form:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AccountModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../account/account-form/account-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AddressModel&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../shipping/address-form/address-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PreferencesModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../account/preferences-form/preferences-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-profile-form&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProfileFormComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Profile&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AccountModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AddressModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PreferencesModel&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each section uses a variable to set the initial value.&lt;/p&gt;

&lt;p&gt;Since each form is its own reusable component, we store the interface and the initial model value with that component so we can access and maintain it near the component rather than wire it up uniquely in every form it’s used in.&lt;/p&gt;

&lt;p&gt;This was one of the key concepts from the previous example.&lt;/p&gt;

&lt;p&gt;Below the model signal we create the form itself:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildAccountSection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../account/account-form/account-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildAddressSection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../shipping/address-form/address-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildPreferencesSection&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;../../account/preferences-form/preferences-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-profile-form&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProfileFormComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;buildAccountSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;buildAddressSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;buildPreferencesSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;//...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And here’s the other main concept, each section exports a function that defines its validation.&lt;/p&gt;

&lt;p&gt;These live with the components themselves, just like the interface and initial values.&lt;/p&gt;

&lt;p&gt;That way the parent form can compose them easily without redefining them everywhere the components are used.&lt;/p&gt;

&lt;h2 id=&quot;inside-a-reusable-angular-form-section-component&quot;&gt;Inside a Reusable Angular Form Section Component&lt;/h2&gt;

&lt;p&gt;In the original implementation, &lt;a href=&quot;https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/blob/master/src/app/account/account-form/account-form.component.ts&quot; target=&quot;_blank&quot;&gt;the account form component&lt;/a&gt; had an input to take in the account field tree from the parent:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-account-form&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AccountFormComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;FieldTree&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The component expects the parent to pass in the account portion of the form.&lt;/p&gt;

&lt;p&gt;In &lt;a href=&quot;https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/blob/master/src/app/account/account-form/account-form.component.html&quot; target=&quot;_blank&quot;&gt;the template&lt;/a&gt;, each input is bound using the &lt;a href=&quot;https://angular.dev/api/forms/signals/FormField&quot; target=&quot;_blank&quot;&gt;FormField&lt;/a&gt; directive accessing the appropriate field from the input:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
    First Name
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().firstName&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-validation-errors&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[fieldState]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().firstName()&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
    Last Name
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().lastName&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-validation-errors&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[fieldState]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().lastName()&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Validation errors are shown by passing the control state to a custom &lt;a href=&quot;https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/tree/master/src/app/shared/validation-errors&quot; target=&quot;_blank&quot;&gt;validation errors component&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Within each form section component we have a &lt;strong&gt;model file&lt;/strong&gt; that contains three things:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;The interface&lt;/strong&gt; - used to strictly type this section of the form&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The initial value object&lt;/strong&gt; - used when this section is added to the parent form model signal&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;A validation builder function&lt;/strong&gt; - takes a schema path tree typed with the section interface and defines required fields, patterns, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, the account form model file looks like this:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SchemaPathTree&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms/signals&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;firstName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;lastName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AccountModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;firstName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;lastName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildAccountSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SchemaPathTree&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;firstName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;First name is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Last name is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The main idea is as much of the form logic as possible lives with the component itself.&lt;/p&gt;

&lt;p&gt;Anything needed to wire up the form in the parent is exported from the component so it doesn’t have to be manually recreated everywhere it’s used.&lt;/p&gt;

&lt;p&gt;The address and preferences form components are set up the same way.&lt;/p&gt;

&lt;p&gt;That was the whole concept.&lt;/p&gt;

&lt;p&gt;But now we’re going to try something different.&lt;/p&gt;

&lt;h2 id=&quot;refactoring-to-angular-formvaluecontrol&quot;&gt;Refactoring to Angular FormValueControl&lt;/h2&gt;

&lt;p&gt;Instead of passing field trees into the section components, we can turn each section into a custom form control.&lt;/p&gt;

&lt;p&gt;Angular provides an interface for this called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormValueControl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start with the account section.&lt;/p&gt;

&lt;p&gt;First, we add the interface to the component and type it with our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Account&lt;/code&gt; interface:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormValueControl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms/signals&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({...})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AccountFormComponent&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormValueControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When you implement this interface, Angular expects the component to expose a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; &lt;a href=&quot;https://angular.dev/api/core/model&quot; target=&quot;_blank&quot;&gt;model input&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So we replace the old input with a new model input that represents the entire value of the account section:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({...})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AccountFormComponent&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormValueControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AccountModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, we’ll create a local form using the &lt;a href=&quot;https://angular.dev/api/forms/signals/form&quot; target=&quot;_blank&quot;&gt;form()&lt;/a&gt; function from the Signal Forms API and move the validation from the model file into this form:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;firstName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;First name is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Last name is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At this point the component owns its own validation completely which sounds nice, because the parent form doesn’t have to know anything about the internal fields.&lt;/p&gt;

&lt;p&gt;The validation moves out of the model file and into the component.&lt;/p&gt;

&lt;p&gt;In the template, we just need to update the bindings to use the local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; property instead of the input signal:&lt;/p&gt;

&lt;h4 id=&quot;before&quot;&gt;Before:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().firstName&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after&quot;&gt;After:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.firstName&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The address and preferences form components follow the same pattern.&lt;/p&gt;

&lt;p&gt;The address form implements &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormValueControl&lt;/code&gt;, switches to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; model input, and creates a local form with its validation:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-address-form&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;AddressFormComponent&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormValueControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AddressModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;street&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Street is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;city&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;City is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;State is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ZIP code is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\d{5}&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;$/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ZIP code must be 5 digits&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The preferences form is simpler because it has no validation:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/core&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormValueControl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms/signals&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PreferencesModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./preferences-form.model&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-preferences-form&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;PreferencesFormComponent&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormValueControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Preferences&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Preferences&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;PreferencesModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once all section components are converted to custom controls, we update the parent form to use them as such.&lt;/p&gt;

&lt;h2 id=&quot;connecting-formvaluecontrol-to-the-parent-signal-form&quot;&gt;Connecting FormValueControl to the Parent Signal Form&lt;/h2&gt;

&lt;p&gt;Since the child components now own their validation, the parent no longer needs to call the build functions.&lt;/p&gt;

&lt;p&gt;We can remove those from the form definition.&lt;/p&gt;

&lt;h4 id=&quot;before-1&quot;&gt;Before:&lt;/h4&gt;
&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;buildAccountSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;buildAddressSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;buildPreferencesSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after-1&quot;&gt;After:&lt;/h4&gt;
&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we need to import the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormField&lt;/code&gt; directive so that we can use it in the template:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormField&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms/signals&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-profile-form&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;imports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;FormField&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we can update the template to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormField&lt;/code&gt; directive instead of the old input:&lt;/p&gt;

&lt;h4 id=&quot;before-2&quot;&gt;Before:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formRoot]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-account-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[form]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().account&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-address-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[form]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().shippingAddress&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-preferences-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[form]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().preferences&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after-2&quot;&gt;After:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formRoot]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-account-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.account&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-address-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.shippingAddress&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;app-preferences-form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.preferences&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, after we save, everything still looks the same:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/profile-form-formvaluecontrol.jpg&quot; alt=&quot;The profile form with the account information, shipping address, and preferences sections&quot; width=&quot;1524&quot; height=&quot;1406&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The debug panel form object is unchanged.&lt;/p&gt;

&lt;p&gt;Typing in the first name updates the value, so the custom control is working:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/first-name-input-formvaluecontrol.jpg&quot; alt=&quot;The first name input field with the value updating in real time&quot; width=&quot;1044&quot; height=&quot;544&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;But here’s the problem.&lt;/p&gt;

&lt;p&gt;When I click in and blur the last name field, we see the validation error inside the component which is correct:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/last-name-validation-error-formvaluecontrol.jpg&quot; alt=&quot;The last name input field with the validation error&quot; width=&quot;988&quot; height=&quot;606&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;But in the debug panel, the parent form still says valid:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/debug-panel-formvaluecontrol.jpg&quot; alt=&quot;The debug panel showing the form object with the account information, shipping address, and preferences sections&quot; width=&quot;1102&quot; height=&quot;632&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The parent form has no idea those fields are required.&lt;/p&gt;

&lt;p&gt;The validation only exists inside the child form, so the parent doesn’t see it.&lt;/p&gt;

&lt;p&gt;In my opinion, this is the biggest drawback of this approach.&lt;/p&gt;

&lt;p&gt;The only fix I’ve found isn’t great.&lt;/p&gt;

&lt;h3 id=&quot;why-parent-form-validation-breaks-with-formvaluecontrol&quot;&gt;Why Parent Form Validation Breaks with FormValueControl&lt;/h3&gt;

&lt;p&gt;I had to add validation back within the parent’s builder functions:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildAccountSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SchemaPathTree&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;firstName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lastName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We don’t need the error messages there because they already exist in the form section where they’re displayed.&lt;/p&gt;

&lt;p&gt;But now things get awkward, we’re duplicating validation logic.&lt;/p&gt;

&lt;p&gt;In the address form it’s worse:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buildAddressSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SchemaPathTree&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Address&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;street&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;city&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\d{5}&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;$/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We have a regular expression duplicated in two places.&lt;/p&gt;

&lt;p&gt;We also need to add the builder functions back into the parent form to wire up this validation:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;buildAccountSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;buildAddressSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;buildPreferencesSection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After doing that, the form starts out invalid correctly now:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/profile-form-formvaluecontrol-invalid.jpg&quot; alt=&quot;The profile form with the account information, shipping address, and preferences sections&quot; width=&quot;914&quot; height=&quot;552&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Validation errors show when we blur required fields:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/last-name-validation-error-formvaluecontrol.jpg&quot; alt=&quot;The last name input field with the validation error&quot; width=&quot;988&quot; height=&quot;606&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And the form status becomes valid once all data is filled in:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-12/profile-form-formvaluecontrol-valid.jpg&quot; alt=&quot;The profile form with the account information, shipping address, and preferences sections&quot; width=&quot;1258&quot; height=&quot;1198&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Everything works, but we’ve duplicated validation logic to get there.&lt;/p&gt;

&lt;h2 id=&quot;should-you-use-formvaluecontrol-for-angular-signal-form-sections&quot;&gt;Should You Use FormValueControl for Angular Signal Form Sections?&lt;/h2&gt;

&lt;p&gt;You can absolutely build reusable form sections using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormValueControl&lt;/code&gt;, and technically it works.&lt;/p&gt;

&lt;p&gt;But for this specific scenario it doesn’t actually simplify things.&lt;/p&gt;

&lt;p&gt;We ended up duplicating validation logic so the parent form could still understand the overall validity.&lt;/p&gt;

&lt;p&gt;The original approach, passing field tree slices into section components, might still be the cleaner architecture for large forms.&lt;/p&gt;

&lt;p&gt;If you’ve found a better way to solve this with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormValueControl&lt;/code&gt;, I’d genuinely love to hear it.&lt;/p&gt;

&lt;h2 id=&quot;learn-angular-signal-forms-indepth&quot;&gt;Learn Angular Signal Forms in depth&lt;/h2&gt;

&lt;p&gt;If you’d like to go deeper, I created a full course that walks through building a real Signal Form from scratch.&lt;/p&gt;

&lt;p&gt;It covers:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;validation patterns &lt;/li&gt;
  &lt;li&gt;async validation &lt;/li&gt;
  &lt;li&gt;dynamic forms &lt;/li&gt;
  &lt;li&gt;custom controls &lt;/li&gt;
  &lt;li&gt;submission and server errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check it out here:
👉 &lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/fZZ1UVkyB4I?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Signal Forms Course Preview&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/angular-signal-forms-structuring-large-forms/&quot;&gt;How to Structure Large Forms Without Losing Your Mind&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867&quot; target=&quot;_blank&quot;&gt;My course “Angular Signal Forms: Build Modern Forms with Signals”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;n a &lt;a href=&quot;/angular-signal-forms-structuring-large-forms/&quot;&gt;recent guide&lt;/a&gt; I showed a pattern for building large &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; using reusable form sections. But a common follow-up question kept coming up: &lt;em&gt;Why not just use &lt;a href=&quot;https://angular.dev/api/forms/signals/FormValueControl&quot; target=&quot;_blank&quot;&gt;FormValueControl&lt;/a&gt; instead?&lt;/em&gt; It sounded like a great idea, so I tried it. In this post you&apos;ll see how it works and why I&apos;m not completely convinced it&apos;s actually the better approach for this scenario.&lt;/p&gt;

</description><pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-signal-forms-formvaluecontrol-form-sections/</link>
        <guid isPermaLink="true">https://briantree.se/angular-signal-forms-formvaluecontrol-form-sections/</guid><category>Angular</category><category>Angular Forms</category><category>Signal Forms</category><category>Form Validation</category><category>TypeScript</category></item><item>
        <title>Angular 21 Signal Forms: ignoreValidators Explained</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;W&lt;/span&gt;hat happens if a user clicks submit while your async validator is still checking the server? Do you submit bad data? Block the user? Silently fail? &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; now gives you explicit control over that behavior with the &lt;code&gt;ignoreValidators&lt;/code&gt; option. In this guide, we&apos;ll walk through all three modes so you can choose the right strategy for your real-world Angular applications.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/Fd_8YGeFMTk?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular 21 Signal Forms: ignoreValidators Explained&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;angular-signal-forms-default-submission-behavior-with-async-validators&quot;&gt;Angular Signal Forms: Default Submission Behavior with Async Validators&lt;/h2&gt;

&lt;p&gt;Here we have a simple sign-up form with a username field:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-before.jpg&quot; alt=&quot;The signup form with username field&quot; width=&quot;1244&quot; height=&quot;696&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;If you type something short, you get a minlength validation error:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-minlength-error.jpg&quot; alt=&quot;The signup form with username field and a minlength validation error&quot; width=&quot;1402&quot; height=&quot;752&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;When you type something longer, a “checking availability” message appears:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-checking-availability.jpg&quot; alt=&quot;The signup form with username field and a checking availability message&quot; width=&quot;1401&quot; height=&quot;750&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;This message comes from an async validator that checks whether the username is already taken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s the key behavior to understand:&lt;/strong&gt; If you click the submit button while the async validator is still running, the submit action runs immediately:&lt;/p&gt;

&lt;p&gt;You can see this because we log the form value on submission:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-submission-log.jpg&quot; alt=&quot;The signup form with username field and a submission log&quot; width=&quot;2276&quot; height=&quot;760&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;It does not wait for the async validator to complete.&lt;/p&gt;

&lt;p&gt;Once the async check finishes, we get a validation error letting us know the name is already taken but by then, submission has already happened:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-submission-error.jpg&quot; alt=&quot;The signup form with username field and a submission error&quot; width=&quot;1282&quot; height=&quot;756&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;This is the default behavior we’ll modify in this tutorial.&lt;/p&gt;

&lt;p&gt;But first, let’s see how the form is configured.&lt;/p&gt;

&lt;h2 id=&quot;how-the-signal-form-is-configured-template--submission-logic&quot;&gt;How the Signal Form Is Configured (Template + Submission Logic)&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-submit-options/blob/master/src/form/form.component.html&quot; target=&quot;_blank&quot;&gt;The template&lt;/a&gt; uses the new &lt;a href=&quot;https://angular.dev/api/forms/signals/FormRoot&quot; target=&quot;_blank&quot;&gt;FormRoot&lt;/a&gt; directive on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element to connect the native form to the Signal Form model:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formRoot]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The text input uses the &lt;a href=&quot;https://angular.dev/api/forms/signals/FormField&quot; target=&quot;_blank&quot;&gt;FormField&lt;/a&gt; directive to bind to the username field:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;username&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.username&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Below the input, we show a “checking availability” message when &lt;code&gt;form.username().pending()&lt;/code&gt; is true:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@if (form.username().pending()) {
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;info&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Checking availability...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This happens while the async validator is running.&lt;/p&gt;

&lt;p&gt;We then loop through errors when the field has been touched to show validation errors:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; @if (form.username().touched() &lt;span class=&quot;err&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; form.username().invalid()) {
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error-list&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        @for (err of form.username().errors(); track $index) {
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{{ err.message }}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        }
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The submit button is disabled during submission and its label changes to “Creating…”:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;submit&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[disabled]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().submitting()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt; 
    {{ form().submitting() ? &apos;Creating…&apos; : &apos;Create account&apos; }}
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;the-typescript-model-and-validation&quot;&gt;The TypeScript: Model and Validation&lt;/h3&gt;

&lt;p&gt;In &lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-submit-options/blob/master/src/form/form.component.ts&quot; target=&quot;_blank&quot;&gt;the component&lt;/a&gt;, we first define the form model for the form:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;nl&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Then we create the form using the &lt;a href=&quot;https://angular.dev/api/forms/signals/form&quot; target=&quot;_blank&quot;&gt;form()&lt;/a&gt; function and pass in the model.&lt;/p&gt;

&lt;p&gt;We also add a required, minlength, and async validators to the username field:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Please enter a username&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;minLength&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
            &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Your username must be at least 3 characters&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;validateAsync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;factory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
                    &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;na&quot;&gt;loader&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
                        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;available&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checkUsernameAvailability&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;available&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;onSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;username_taken&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;This username is already taken&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Validation error:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;debounce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And then, the third parameter to this form is an options object where we handle submission:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;submission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Value:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This &lt;code&gt;submission&lt;/code&gt; object defines an &lt;code&gt;action&lt;/code&gt; that runs when submission is allowed.&lt;/p&gt;

&lt;p&gt;This action only runs if the form permits it, and by default, it runs even when async validators are still pending.&lt;/p&gt;

&lt;p&gt;Validation errors can appear after submission has already happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is that what you want in a real application?&lt;/strong&gt; Sometimes yes, if you want a responsive feel and you’re okay validating after submission.&lt;/p&gt;

&lt;p&gt;Often you need stricter behavior, and until now there was no control over this.&lt;/p&gt;

&lt;p&gt;But now, the &lt;code&gt;ignoreValidators&lt;/code&gt; option changes that.&lt;/p&gt;

&lt;h2 id=&quot;ignorevalidators-pending---default-async-submission-behavior-explained&quot;&gt;ignoreValidators: ‘pending’ - Default Async Submission Behavior Explained&lt;/h2&gt;

&lt;p&gt;The new &lt;code&gt;ignoreValidators&lt;/code&gt; option determines how validator state affects whether submission is allowed.&lt;/p&gt;

&lt;p&gt;Inside the &lt;code&gt;submission&lt;/code&gt; configuration is where you control how validator state affects whether submission is allowed.&lt;/p&gt;

&lt;p&gt;The three possible values are &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;, and &lt;code&gt;all&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;ignoreValidators: &apos;pending&apos;&lt;/code&gt;, you can still submit while async validators are running.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;submission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;ignoreValidators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;pending&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After saving, you’ll notice that the form still submits while the async validator is still running:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-submission-while-async-validator-is-running.jpg&quot; alt=&quot;The signup form with username field and a submission while the async validator is still running&quot; width=&quot;2266&quot; height=&quot;754&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;This is because &lt;code&gt;pending&lt;/code&gt; is the default behavior.&lt;/p&gt;

&lt;p&gt;Adding it explicitly doesn’t change behavior, but it can make the intent clear in your codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use this when:&lt;/strong&gt; You want a fast, responsive experience and don’t want to block on async checks.&lt;/p&gt;

&lt;h2 id=&quot;ignorevalidators-none---block-submission-until-validation-completes&quot;&gt;ignoreValidators: ‘none’ - Block Submission Until Validation Completes&lt;/h2&gt;

&lt;p&gt;When we switch to &lt;code&gt;ignoreValidators: &apos;none&apos;&lt;/code&gt; we get more strict, production-safe behavior:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;submission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;ignoreValidators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After saving, you’ll notice that the form does not submit while the async validator is still running anymore:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-not-submitting-while-async-validator-is-running.jpg&quot; alt=&quot;The signup form with username field not submitting while the async validator is still running&quot; width=&quot;2254&quot; height=&quot;762&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And it won’t submit once validation finishes if the form is still invalid:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-not-submitting-with-invalid-fields.jpg&quot; alt=&quot;The signup form with username field not submitting with invalid fields&quot; width=&quot;2250&quot; height=&quot;756&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;none&lt;/code&gt;, the form respects all validator states.&lt;/p&gt;

&lt;p&gt;If a validator is pending, submission waits.&lt;/p&gt;

&lt;p&gt;If the form is invalid, submission is blocked.&lt;/p&gt;

&lt;p&gt;The user cannot submit until all validation (including async) has completed and the form is valid.&lt;/p&gt;

&lt;h3 id=&quot;using-oninvalid-to-auto-focus-the-first-invalid-field&quot;&gt;Using onInvalid to Auto-Focus the First Invalid Field&lt;/h3&gt;

&lt;p&gt;When submission is blocked because the form is invalid, you can improve UX by focusing the first invalid field.&lt;/p&gt;

&lt;p&gt;You can add an &lt;code&gt;onInvalid&lt;/code&gt; callback to do this:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;submission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;ignoreValidators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;none&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;onInvalid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;detail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;first&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;detail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;errorSummary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()?.[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldTree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;focusBoundControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.();&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, when the user clicks submit and the form is invalid, &lt;code&gt;onInvalid&lt;/code&gt; runs.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;errorSummary()&lt;/code&gt; to get a flat list of fields with errors, grab the first one, and use &lt;code&gt;focusBoundControl()&lt;/code&gt; to move focus to it:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-submission-focusing-first-invalid-field.gif&quot; alt=&quot;The signup form with username field and submission focusing the first invalid field&quot; width=&quot;1272&quot; height=&quot;778&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;This gives users clear feedback about what needs to be fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use this when:&lt;/strong&gt; You need strict, production-safe validation and want to prevent invalid or partially validated data from being submitted.&lt;/p&gt;

&lt;h2 id=&quot;ignorevalidators-all---submit-even-when-invalid-or-pending&quot;&gt;ignoreValidators: ‘all’ - Submit Even When Invalid or Pending&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;ignoreValidators: &apos;all&apos;&lt;/code&gt;, the form submits regardless of validation state:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;submission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;ignoreValidators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this mode, you can submit with an empty required field, or while an async validator is still running, it essentially ignores the validator state:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/03-05/signup-form-submission-with-empty-required-field.gif&quot; alt=&quot;The signup form with username field and a submission with empty required field&quot; width=&quot;1908&quot; height=&quot;600&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Validation still runs and errors still appear, but submission has already occurred.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use this when:&lt;/strong&gt; You’re building features like saving drafts, auto-saving, or partial data persistence, not for forms that require full validation before submission.&lt;/p&gt;

&lt;h2 id=&quot;choosing-the-right-ignorevalidators-mode-for-real-world-angular-apps&quot;&gt;Choosing the Right ignoreValidators Mode for Real-World Angular Apps&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Mode&lt;/th&gt;
      &lt;th&gt;Behavior&lt;/th&gt;
      &lt;th&gt;Best For&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pending&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Submit while async validators run; block only on invalid sync state&lt;/td&gt;
      &lt;td&gt;Fast UX when you’re okay validating after submit&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;none&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Block until all validation (sync + async) completes&lt;/td&gt;
      &lt;td&gt;Production forms that must not submit invalid data&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Submit regardless of validation state&lt;/td&gt;
      &lt;td&gt;Drafts, auto-save, partial persistence&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code&gt;ignoreValidators&lt;/code&gt; gives you precise control over what happens when users submit while validation is still running.&lt;/p&gt;

&lt;p&gt;If you’re building real-world Angular applications with Signal Forms, this is an option you should understand deeply.&lt;/p&gt;

&lt;p&gt;If this helped you, be sure to &lt;a href=&quot;https://www.youtube.com/c/briantreese?sub_confirmation=1&quot;&gt;subscribe&lt;/a&gt; for more deep dives into modern Angular features.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-submit-options&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms Guide&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://angular.dev/guide/forms/validation&quot; target=&quot;_blank&quot;&gt;Angular Form Validation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jdoqocy.com/click-101557355-17135603&quot; target=&quot;_blank&quot;&gt;Get a Pluralsight FREE TRIAL HERE!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;W&lt;/span&gt;hat happens if a user clicks submit while your async validator is still checking the server? Do you submit bad data? Block the user? Silently fail? &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; now gives you explicit control over that behavior with the &lt;code&gt;ignoreValidators&lt;/code&gt; option. In this guide, we&apos;ll walk through all three modes so you can choose the right strategy for your real-world Angular applications.&lt;/p&gt;

</description><pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-signal-forms-ignorevalidators-async-validation/</link>
        <guid isPermaLink="true">https://briantree.se/angular-signal-forms-ignorevalidators-async-validation/</guid><category>Angular</category><category>Angular Forms</category><category>Signal Forms</category><category>Async Validation</category><category>Form Validation</category><category>Angular 21</category><category>TypeScript</category></item><item>
        <title>Angular Signal Forms: Number Inputs Finally Fixed in Angular 21.2</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;N&lt;/span&gt;umber inputs in &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; had a subtle but frustrating problem, there was no clean way to represent an empty numeric field. But, as of &lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0&quot; target=&quot;_blank&quot;&gt;v21.2.0&lt;/a&gt;, this issue has been quietly fixed. Let me show you exactly what this looked like before, and then follow it up with how it works now.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/3DlNnIsUsMM?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular Signal Forms: Number Inputs Finally Fixed in Angular 21.2&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;the-starting-point-a-simple-signup-form&quot;&gt;The Starting Point: A Simple Signup Form&lt;/h2&gt;

&lt;p&gt;To demonstrate the problem, let’s start with a basic signup form that has a username and email field:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-before.jpg&quot; alt=&quot;The signup form with username and email fields&quot; width=&quot;1174&quot; height=&quot;1108&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;At the bottom of the form, we’re outputting the form model value so we can see exactly what’s happening in real time as we interact with the form:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-model-output.jpg&quot; alt=&quot;The form model output showing the username and email fields&quot; width=&quot;1306&quot; height=&quot;388&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Now let’s say we need to add an age field.&lt;/p&gt;

&lt;p&gt;Seems simple enough, just add a number input, right?&lt;/p&gt;

&lt;p&gt;But this is where things get interesting.&lt;/p&gt;

&lt;h2 id=&quot;the-obvious-solution-zero&quot;&gt;The Obvious Solution (zero)&lt;/h2&gt;

&lt;p&gt;In the &lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-null-number-example/blob/master/src/form/form.component.ts&quot; target=&quot;_blank&quot;&gt;component TypeScript&lt;/a&gt;, we have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SignupModel&lt;/code&gt; interface that defines the shape of our form data:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;nl&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;nl&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, we want to add a new property for age.&lt;/p&gt;

&lt;p&gt;Since this is a number field, I’ll type it as a number:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;nl&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;nl&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;nl&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, we need to initialize the form model signal with a default value for the age field.&lt;/p&gt;

&lt;p&gt;Since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; is a number, the most obvious default value is zero:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We also need validation.&lt;/p&gt;

&lt;p&gt;Inside the form configuration function, we need to add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;required&lt;/code&gt; validator for this new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; field:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Please enter an age&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This ensures that the user must provide a value for this field.&lt;/p&gt;

&lt;h3 id=&quot;adding-a-number-input-using-angular-signal-forms&quot;&gt;Adding a Number Input Using Angular Signal Forms&lt;/h3&gt;

&lt;p&gt;In the template, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; field follows the same pattern as the other inputs:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;field&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    @let age = form.age();
    @let showAgeError = age.touched() &lt;span class=&quot;err&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; age.invalid();
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;label&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;for=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Age&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;number&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.age&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    @if (showAgeError) {
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error-list&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
            @for (error of age.errors(); track error.kind) {
                &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
            }
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    }
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We create an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; template variable to reference the age field signal, and then a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;showAgeError&lt;/code&gt; variable to check if the field has been touched and is invalid.&lt;/p&gt;

&lt;p&gt;Then, we add a label and number input for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formField&lt;/code&gt; directive connects the input to the Signal Form control, just like the other fields.&lt;/p&gt;

&lt;p&gt;Finally, we add a conditional error message loop that displays validation errors once the field has been touched and is invalid.&lt;/p&gt;

&lt;h3 id=&quot;the-problem-with-zero&quot;&gt;The Problem with Zero&lt;/h3&gt;

&lt;p&gt;This works, but there’s an immediate issue: The age field renders with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt; pre-filled:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-age-field-zero.jpg&quot; alt=&quot;The signup form with age field pre-filled with 0&quot; width=&quot;1274&quot; height=&quot;650&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The form model output also shows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age: 0&lt;/code&gt;:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-model-output-age-zero.jpg&quot; alt=&quot;The form model output showing the age field with a value of 0&quot; width=&quot;1026&quot; height=&quot;420&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;But zero is probably not the user’s actual age.&lt;/p&gt;

&lt;p&gt;It’s a placeholder value that creates ambiguity.&lt;/p&gt;

&lt;p&gt;We can’t distinguish between the user intentionally entering zero and the user not entering anything at all.&lt;/p&gt;

&lt;p&gt;Clearing the field does trigger the required validation error:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-age-field-required-error.jpg&quot; alt=&quot;The signup form with age field blurred showing the required error message&quot; width=&quot;858&quot; height=&quot;378&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And the model value becomes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-model-output-age-null.jpg&quot; alt=&quot;The form model output showing the age field with a value of null&quot; width=&quot;1002&quot; height=&quot;406&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;But starting with zero isn’t ideal for most real-world forms.&lt;/p&gt;

&lt;h2 id=&quot;the-type-safety-compromise-using-a-union-type-string--number&quot;&gt;The Type-Safety Compromise: Using a Union Type (string | number)&lt;/h2&gt;

&lt;p&gt;One workaround is to loosen the type so that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; can be either a number or a string:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then initialize it with an empty string instead of zero:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now the field starts empty:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-age-field-empty.jpg&quot; alt=&quot;The signup form with age field empty&quot; width=&quot;1198&quot; height=&quot;392&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And the model shows an empty string:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-model-output-age-empty-string.jpg&quot; alt=&quot;The form model output showing the age field with a value of empty string&quot; width=&quot;1010&quot; height=&quot;408&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Typing a value works correctly:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-age-field-value.jpg&quot; alt=&quot;The signup form with age field entered with a value&quot; width=&quot;1098&quot; height=&quot;686&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;But we’ve introduced a different problem: from TypeScript’s perspective, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; might be a string.&lt;/p&gt;

&lt;p&gt;We’ve lost type safety, and in a real application, this can lead to bugs and extra conversion logic.&lt;/p&gt;

&lt;h2 id=&quot;the-hack-developers-used-nan&quot;&gt;The Hack Developers Used (NaN)&lt;/h2&gt;

&lt;p&gt;Another approach developers used was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NaN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So we revert the interface back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;number&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we initialize it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NaN&lt;/code&gt; instead of an empty string or zero:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;NaN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The field starts empty again:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-age-field-empty.jpg&quot; alt=&quot;The signup form with age field empty&quot; width=&quot;1198&quot; height=&quot;392&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And the model shows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt; this time, which looks correct:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-model-output-age-null.jpg&quot; alt=&quot;The form model output showing the age field with a value of null&quot; width=&quot;1002&quot; height=&quot;406&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NaN&lt;/code&gt; is semantically misleading.&lt;/p&gt;

&lt;p&gt;It literally means “not a number” while being typed as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;number&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It worked because Angular maps empty number inputs to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;, but Signal Forms didn’t originally support &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt; as a valid model value for number fields.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NaN&lt;/code&gt; was a workaround, not a real solution.&lt;/p&gt;

&lt;h2 id=&quot;the-correct-approach-using-null-for-number-inputs-in-angular-signal-forms&quot;&gt;The Correct Approach: Using null for Number Inputs in Angular Signal Forms&lt;/h2&gt;

&lt;p&gt;As of Angular 21.2.0, Signal Forms officially support &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt; for number inputs.&lt;/p&gt;

&lt;p&gt;This aligns Signal Forms with how Reactive Forms and most backend systems represent optional numeric values.&lt;/p&gt;

&lt;p&gt;We just need to update the interface to allow &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then initialize with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it!&lt;/p&gt;

&lt;p&gt;Now the field still starts empty:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-age-field-empty.jpg&quot; alt=&quot;The signup form with age field empty&quot; width=&quot;1198&quot; height=&quot;392&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And the model value is still &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt; to start:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-26/signup-form-model-output-age-null.jpg&quot; alt=&quot;The form model output showing the age field with a value of null&quot; width=&quot;1002&quot; height=&quot;406&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;There are no more hacks, no ambiguity, and no loss of type safety.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt; clearly represents the absence of a value, which is exactly how most databases and APIs model optional numeric fields.&lt;/p&gt;

&lt;p&gt;Your form state now aligns directly with your data layer.&lt;/p&gt;

&lt;h2 id=&quot;in-summary&quot;&gt;In Summary&lt;/h2&gt;

&lt;p&gt;Angular 21.2.0 quietly shipped a small but meaningful improvement for Signal Forms: proper &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt; support for number inputs.&lt;/p&gt;

&lt;p&gt;It eliminates the need for workarounds and gives you a clean, type-safe way to represent the absence of a numeric value.&lt;/p&gt;

&lt;p&gt;If this helped you, be sure to &lt;a href=&quot;https://www.youtube.com/c/briantreese?sub_confirmation=1&quot;&gt;subscribe&lt;/a&gt; for more deep dives into modern Angular features.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-null-number-example&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-rc.0&quot; target=&quot;_blank&quot;&gt;v21.2.0-rc.0 Release Notes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms Guide&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/angular/angular/releases&quot; target=&quot;_blank&quot;&gt;Angular Releases&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jdoqocy.com/click-101557355-17135603&quot; target=&quot;_blank&quot;&gt;Get a Pluralsight FREE TRIAL HERE!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;N&lt;/span&gt;umber inputs in &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; had a subtle but frustrating problem, there was no clean way to represent an empty numeric field. But, as of &lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0&quot; target=&quot;_blank&quot;&gt;v21.2.0&lt;/a&gt;, this issue has been quietly fixed. Let me show you exactly what this looked like before, and then follow it up with how it works now.&lt;/p&gt;

</description><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-signal-forms-number-input-null/</link>
        <guid isPermaLink="true">https://briantree.se/angular-signal-forms-number-input-null/</guid><category>Angular</category><category>Angular Forms</category><category>Signal Forms</category><category>Angular 21</category><category>TypeScript</category></item><item>
        <title>Angular Signal Forms: The New formRoot Directive Explained</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;F&lt;/span&gt;orm submission in &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; has always required a bit of manual wiring: a submit handler, &lt;code&gt;preventDefault&lt;/code&gt;, and an explicit call to &lt;code&gt;submit()&lt;/code&gt;. It works, but it doesn&apos;t feel fully Angular. Starting in &lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.3&quot; target=&quot;_blank&quot;&gt;Angular 21.2-next.3&lt;/a&gt;, the new &lt;code&gt;formRoot&lt;/code&gt; directive changes that. It makes form submission completely declarative, moves submission logic into the form itself, and eliminates the remaining boilerplate. This post walks through exactly how it works and how to migrate an existing Signal Form in about 60 seconds.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/ARl78Z0XE7M?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular Signal Forms: The New formRoot Directive Explained&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;signal-forms-submission-before-formroot&quot;&gt;Signal Forms Submission Before formRoot&lt;/h2&gt;

&lt;p&gt;Here’s the starting point, a simple signup form with a username and email field:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-before-formroot.jpg&quot; alt=&quot;Signal Forms signup form with username and email fields&quot; width=&quot;1096&quot; height=&quot;882&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;The submit button is disabled until the form is valid:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-submit-button-disabled.jpg&quot; alt=&quot;Signal Forms signup form with submit button disabled due to invalid form state&quot; width=&quot;1246&quot; height=&quot;354&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Once both fields are filled in with valid values, the button enables:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-submit-button-enabled.jpg&quot; alt=&quot;Signal Forms signup form with username and email filled in and submit button enabled&quot; width=&quot;1284&quot; height=&quot;890&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;After we submit the form, in the console, we can see the dirty status, form value, and the model signal value:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-console-output.jpg&quot; alt=&quot;Console showing dirty status, form value, and model signal value after Signal Form submission&quot; width=&quot;1304&quot; height=&quot;308&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Everything works, but the amount of manual wiring required just to submit the form isn’t ideal.&lt;/p&gt;

&lt;h2 id=&quot;manual-form-submission-in-signal-forms-the-old-way&quot;&gt;Manual Form Submission in Signal Forms (The Old Way)&lt;/h2&gt;

&lt;p&gt;In &lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-form-root-example/blob/master/src/form/form.component.html&quot; target=&quot;_blank&quot;&gt;the template&lt;/a&gt;, form submission requires two manual pieces:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(submit)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;onSubmit($event)&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;novalidate&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(submit)&lt;/code&gt; binding wires up a custom method and passes along the event.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;novalidate&lt;/code&gt; attribute disables the browser’s built-in validation so Signal Forms can handle it instead.&lt;/p&gt;

&lt;p&gt;Without it, the browser could show its own popup messages that would conflict with our form validation logic.&lt;/p&gt;

&lt;p&gt;This is what we’ll be changing in this tutorial.&lt;/p&gt;

&lt;p&gt;But just to make sure we understand all aspects of this form before changing it, each input uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formField&lt;/code&gt; directive to connect to the Signal Form:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;username&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.username&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;email&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;email&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form.email&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And the submit button triggers submission:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;submit&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[disabled]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form().invalid() || form().submitting()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt; 
    @if (form().submitting()) {
        Creating… 
    } @else { 
        Create account
    }
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Pretty straightforward stuff.&lt;/p&gt;

&lt;p&gt;Now let’s look at &lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-form-root-example/blob/master/src/form/form.component.ts&quot; target=&quot;_blank&quot;&gt;the TypeScript&lt;/a&gt; for this component.&lt;/p&gt;

&lt;h3 id=&quot;component-typescript&quot;&gt;Component TypeScript&lt;/h3&gt;

&lt;p&gt;In the component class, the form is constructed with a model signal and validation rules:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;nl&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;nl&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;INITIAL_VALUES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;na&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SignupModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;INITIAL_VALUES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Please enter a username&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;minLength&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
            &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Your username must be at least 3 characters&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Please enter an email address&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is the key piece driving everything we just saw in the browser.&lt;/p&gt;

&lt;p&gt;Then the submit handler wires everything together:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;onSubmit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;preventDefault&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;submit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WithOptionalFieldTree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
                    &lt;span class=&quot;na&quot;&gt;fieldTree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
                    &lt;span class=&quot;na&quot;&gt;fieldTree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;errors&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Dirty:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dirty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Value:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Model:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preventDefault&lt;/code&gt; stops the browser from refreshing the page.&lt;/p&gt;

&lt;p&gt;If we didn’t include this, the browser would perform a full page refresh which would wipe out our app state.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit()&lt;/code&gt; call from the Signal Forms API marks fields as touched, runs validation, and executes the submission logic.&lt;/p&gt;

&lt;p&gt;If everything succeeds, we log the dirty status, form value, and model signal value to the console.&lt;/p&gt;

&lt;p&gt;It works, but there are several pieces that require manual wiring just to submit the form.&lt;/p&gt;

&lt;p&gt;The new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formRoot&lt;/code&gt; directive eliminates some of it.&lt;/p&gt;

&lt;h2 id=&quot;angular-formroot-directive-explained-signal-forms&quot;&gt;Angular formRoot Directive Explained (Signal Forms)&lt;/h2&gt;

&lt;p&gt;Starting in Angular 21.2-next.3, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormRoot&lt;/code&gt; directive is now available from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@angular/forms/signals&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;step-1-add-formroot-to-the-component-imports&quot;&gt;Step 1: Add FormRoot to the Component Imports&lt;/h3&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormRoot&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms/signals&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-form&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;imports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormRoot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;step-2-update-the-template&quot;&gt;Step 2: Update the Template&lt;/h3&gt;

&lt;p&gt;Here we remove the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(submit)&lt;/code&gt; binding and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;novalidate&lt;/code&gt; attribute and replace them with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formRoot&lt;/code&gt; directive:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formRoot]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This directive handles both of these automatically.&lt;/p&gt;

&lt;p&gt;That’s the only template change needed.&lt;/p&gt;

&lt;p&gt;Angular now handles submission automatically.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formRoot&lt;/code&gt; prevents default browser submission behavior internally and connects the DOM form to the Signal Form model.&lt;/p&gt;

&lt;h3 id=&quot;step-3-move-submission-logic-into-the-form&quot;&gt;Step 3: Move Submission Logic Into the Form&lt;/h3&gt;

&lt;p&gt;The key shift is that submission logic now lives directly inside the form definition instead of in a separate event handler.&lt;/p&gt;

&lt;p&gt;This makes the form self-contained and removes the need for manual submission wiring in the template.&lt;/p&gt;

&lt;p&gt;This is done with a third argument on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form()&lt;/code&gt; function containing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submission&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;We can essentially move everything from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit()&lt;/code&gt; function in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onSubmit()&lt;/code&gt; method here:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;submission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
                &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signupService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;signup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ValidationError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;WithOptionalFieldTree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;

                    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;nx&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
                            &lt;span class=&quot;na&quot;&gt;fieldTree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                            &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                            &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

                    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;nx&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
                            &lt;span class=&quot;na&quot;&gt;fieldTree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                            &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                            &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fieldErrors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

                    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;errors&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Dirty:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dirty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Value:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Model:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The old &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onSubmit()&lt;/code&gt; method can now be removed entirely.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formRoot&lt;/code&gt; directive connects the DOM &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element to the Signal Form model declaratively.&lt;/p&gt;

&lt;p&gt;When the form is submitted, Angular automatically calls the configured &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submission.action&lt;/code&gt; callback.&lt;/p&gt;

&lt;p&gt;This mirrors how &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formGroup&lt;/code&gt; connects a DOM &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt; in Reactive Forms.&lt;/p&gt;

&lt;h2 id=&quot;formroot-in-action-same-result-less-code&quot;&gt;formRoot in Action: Same Result, Less Code&lt;/h2&gt;

&lt;p&gt;After saving, the form looks exactly the same:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-formroot-form-after-switching-to-formroot.jpg&quot; alt=&quot;The form after migrating to formRoot&quot; width=&quot;1096&quot; height=&quot;882&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And when we submit the form, we see the same console output:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-formroot-console-output-after-submit.jpg&quot; alt=&quot;Console showing the same dirty status, form value, and model signal value after migrating to formRoot and submitting the form&quot; width=&quot;1304&quot; height=&quot;308&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Everything works exactly the same, but with a little less code now.&lt;/p&gt;

&lt;p&gt;At this point, I think there’s one more improvement we can make.&lt;/p&gt;

&lt;h2 id=&quot;resetting-signal-forms-after-submission&quot;&gt;Resetting Signal Forms After Submission&lt;/h2&gt;

&lt;p&gt;After submission, the form keeps its values by default.&lt;/p&gt;

&lt;p&gt;To clear the fields and reset form state, we’ll call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset()&lt;/code&gt; inside the submission action:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;submission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;

                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Dirty:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dirty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Value:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Model:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;

                &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Dirty:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dirty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Value:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Form Model:&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;

                &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This resets the form to its initial state.&lt;/p&gt;

&lt;p&gt;We’ve also added more logs to see the final state of the form and model signals after it’s been reset.&lt;/p&gt;

&lt;p&gt;Now, after we save the form and submit again, it almost looks like nothing changed.&lt;/p&gt;

&lt;p&gt;But if we look closely at the console, we can see that the dirty status actually changed from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-reset-console-output.jpg&quot; alt=&quot;Console showing the dirty status changed from true to false after resetting the form&quot; width=&quot;1356&quot; height=&quot;614&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;What if we also want to reset the form and model value?&lt;/p&gt;

&lt;p&gt;Well, this is pretty easy to do now.&lt;/p&gt;

&lt;h3 id=&quot;how-to-reset-form-state-and-model-signal&quot;&gt;How to Reset Form State AND Model Signal&lt;/h3&gt;

&lt;p&gt;According to the Signal Forms docs, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset()&lt;/code&gt; does several things.&lt;/p&gt;

&lt;p&gt;First, it says that this resets the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;touched&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dirty&lt;/code&gt; states of the field and its descendants.&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-reset-docs-1.jpg&quot; alt=&quot;Signal Forms reset() docs part 1&quot; width=&quot;1882&quot; height=&quot;176&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;And then, optionally we can pass a value to reset the value of the form, which we didn’t and that’s why the value remained the same:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-reset-docs-2.jpg&quot; alt=&quot;Signal Forms reset() docs part 2&quot; width=&quot;1902&quot; height=&quot;172&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;It also mentions that it does not change the data model, it needs to be reset directly, meaning that we need to manually reset the model signal back to its initial value&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-reset-docs-3.jpg&quot; alt=&quot;Signal Forms reset() docs part 3&quot; width=&quot;1796&quot; height=&quot;176&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Kinda clunky… but ok.&lt;/p&gt;

&lt;p&gt;So calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset()&lt;/code&gt; alone will correctly reset &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dirty&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;, but the form fields will still show their submitted values.&lt;/p&gt;

&lt;p&gt;To reset the actual field values, we need to pass the initial data to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset()&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;submission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
                &lt;span class=&quot;nx&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;INITIAL_VALUES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Passing a value to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset()&lt;/code&gt; resets both the form fields back to their initial values.&lt;/p&gt;

&lt;p&gt;Now, what I found during testing is that this appears to handle the model signal automatically now too.&lt;/p&gt;

&lt;p&gt;No need to reset the model signal separately.&lt;/p&gt;

&lt;p&gt;This appears to be an intentional improvement in recent Angular releases.&lt;/p&gt;

&lt;p&gt;And now after submitting, the fields clear out and both the form value and model signal return to their initial empty state:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-19/signal-form-reset-after-submit.jpg&quot; alt=&quot;Signal Form fields cleared and console showing reset dirty status, empty form value, and empty model signal after submission&quot; width=&quot;1136&quot; height=&quot;428&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;h2 id=&quot;why-formroot-is-probably-the-new-recommended-pattern&quot;&gt;Why formRoot Is Probably the New Recommended Pattern&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formRoot&lt;/code&gt; directive removes the last piece of manual boilerplate from Signal Forms submission.&lt;/p&gt;

&lt;p&gt;Declaring submission logic inside the form rather than in a separate method keeps everything in one place, which is easier to read and easier to test.&lt;/p&gt;

&lt;p&gt;You can still use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit()&lt;/code&gt; function directly when you need fine-grained control, but for most forms, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formRoot&lt;/code&gt; is the cleaner path forward.&lt;/p&gt;

&lt;p&gt;It also aligns Signal Forms more closely with Angular’s existing Reactive Forms mental model, where directives like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formGroup&lt;/code&gt; declaratively connect the DOM to the form model.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formRoot&lt;/code&gt; follows the same idea.&lt;/p&gt;

&lt;p&gt;If this helped you, be sure to &lt;a href=&quot;https://www.youtube.com/c/briantreese?sub_confirmation=1&quot;&gt;subscribe&lt;/a&gt; for more deep dives into modern Angular features.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-form-root-example&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.3&quot; target=&quot;_blank&quot;&gt;v21.2.0-next.3 Release Notes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms Guide&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jdoqocy.com/click-101557355-17135603&quot; target=&quot;_blank&quot;&gt;Get a Pluralsight FREE TRIAL HERE!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;F&lt;/span&gt;orm submission in &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; has always required a bit of manual wiring: a submit handler, &lt;code&gt;preventDefault&lt;/code&gt;, and an explicit call to &lt;code&gt;submit()&lt;/code&gt;. It works, but it doesn&apos;t feel fully Angular. Starting in &lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.3&quot; target=&quot;_blank&quot;&gt;Angular 21.2-next.3&lt;/a&gt;, the new &lt;code&gt;formRoot&lt;/code&gt; directive changes that. It makes form submission completely declarative, moves submission logic into the form itself, and eliminates the remaining boilerplate. This post walks through exactly how it works and how to migrate an existing Signal Form in about 60 seconds.&lt;/p&gt;

</description><pubDate>Thu, 19 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-signal-forms-formroot-directive/</link>
        <guid isPermaLink="true">https://briantree.se/angular-signal-forms-formroot-directive/</guid><category>Angular</category><category>Angular Forms</category><category>Signal Forms</category><category>Angular Form Submission</category><category>formRoot</category><category>Angular 21</category></item><item>
        <title>Bridge Signal Forms and Reactive Forms in Angular 21.2</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;W&lt;/span&gt;hat if you could start using &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; inside an existing &lt;a href=&quot;https://angular.dev/guide/forms/reactive-forms&quot; target=&quot;_blank&quot;&gt;Reactive Form&lt;/a&gt; without rewriting the entire thing? Well, in &lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.0&quot; target=&quot;_blank&quot;&gt;Angular 21.2-next&lt;/a&gt;, you can! The new &lt;code&gt;SignalFormControl&lt;/code&gt; class acts as a compatibility bridge, letting you mix signal-backed fields into a traditional &lt;code&gt;FormGroup&lt;/code&gt; while keeping your existing form structure intact. This post walks through how it works, how to add it to an existing form, and what changes in the template.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/8e008s6rr1Y?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Bridge Signal Forms and Reactive Forms in Angular 21.2&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;This approach allows Angular developers to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SignalFormControl&lt;/code&gt; for Reactive Forms compatibility without a full migration.&lt;/p&gt;

&lt;h2 id=&quot;current-angular-reactive-form-example-baseline-setup&quot;&gt;Current Angular Reactive Form Example (Baseline Setup)&lt;/h2&gt;

&lt;p&gt;In this example, we’ll work with a profile form built entirely with the Reactive Forms module.&lt;/p&gt;

&lt;p&gt;The form has three sections: Account Information, Shipping Address, and Preferences:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-12/profile-form-reactive-baseline.jpg&quot; alt=&quot;The profile form with the account information, shipping address, and preferences sections&quot; width=&quot;1796&quot; height=&quot;1380&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;Interacting with the controls in this form updates the debug panel in real time:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-12/reactive-form-updating-real-time.jpg&quot; alt=&quot;Profile form with data entered in account, shipping address, and preferences, and the submit button enabled&quot; width=&quot;1788&quot; height=&quot;790&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;As soon as everything required is filled in, the form becomes valid and the submit button enables:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-12/reactive-form-valid-submit-button-enabled.jpg&quot; alt=&quot;Profile form with all fields filled out and the form is valid so the submit button is enabled&quot; width=&quot;2234&quot; height=&quot;994&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;This is a standard Angular Reactive Form.&lt;/p&gt;

&lt;p&gt;Let’s look at how it’s built.&lt;/p&gt;

&lt;h2 id=&quot;how-this-reactive-form-is-built-formgroup--formcontrol&quot;&gt;How This Reactive Form Is Built (FormGroup + FormControl)&lt;/h2&gt;

&lt;p&gt;In &lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-compat-example/blob/master/src/app/sign-up/profile-form/profile-form.component.ts&quot; target=&quot;_blank&quot;&gt;the component TypeScript&lt;/a&gt;, we have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProfileForm&lt;/code&gt; interface:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProfileForm&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;firstName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;lastName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;street&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;marketingOptIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The form is constructed using &lt;a href=&quot;https://angular.dev/api/forms/FormGroup&quot; target=&quot;_blank&quot;&gt;FormGroup&lt;/a&gt; for each section and &lt;a href=&quot;https://angular.dev/api/forms/FormControl&quot; target=&quot;_blank&quot;&gt;FormControl&lt;/a&gt; for each field:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProfileFormComponent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ProfileForm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;firstName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;lastName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;street&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;marketingOptIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;nonNullable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The first name, last name, and street address fields are required.&lt;/p&gt;

&lt;p&gt;The marketing opt-in field is not.&lt;/p&gt;

&lt;p&gt;That’s the form configuration, now let’s look at the template.&lt;/p&gt;

&lt;h2 id=&quot;how-formgroup-and-formcontrolname-work-in-angular&quot;&gt;How formGroup and formControlName Work in Angular&lt;/h2&gt;

&lt;p&gt;In &lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-compat-example/blob/master/src/app/sign-up/profile-form/profile-form.component.html&quot; target=&quot;_blank&quot;&gt;the template&lt;/a&gt;, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formGroup&lt;/code&gt; directive binds the DOM form to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt; instance:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formGroup]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(submit)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;onSubmit($event)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formGroupName&lt;/code&gt; directive targets the nested “account” section:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;field-group&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;formGroupName=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;account&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and within it, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formControlName&lt;/code&gt; wires each input to its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;formControlName=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;firstName&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A template variable holds the control for reuse, then we check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;touched&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;errors&lt;/code&gt; to display validation messages:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@let firstNameControl = form.controls.account.controls.firstName;
@if (firstNameControl.touched &lt;span class=&quot;err&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; firstNameControl.errors) {
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;First name is required&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The same pattern repeats for the street field and the preferences section.&lt;/p&gt;

&lt;p&gt;Classic Reactive Forms.&lt;/p&gt;

&lt;p&gt;Nothing new, nothing fancy, totally predictable.&lt;/p&gt;

&lt;h2 id=&quot;why-migrating-large-reactive-forms-to-signal-forms-is-hard&quot;&gt;Why Migrating Large Reactive Forms to Signal Forms Is Hard&lt;/h2&gt;

&lt;p&gt;This form is moderately complex, but imagine it’s much larger.&lt;/p&gt;

&lt;p&gt;Imagine it has many fields, nested groups, and custom components everywhere.&lt;/p&gt;

&lt;p&gt;Fully migrating to Signal Forms would be painful and, potentially, not feasible in the short term.&lt;/p&gt;

&lt;p&gt;Yet you want to start using Signal Forms for new features.&lt;/p&gt;

&lt;p&gt;Until Angular 21.2, there wasn’t a clean way to mix the two.&lt;/p&gt;

&lt;p&gt;The new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SignalFormControl&lt;/code&gt; class solves that.&lt;/p&gt;

&lt;p&gt;It’s a compatibility wrapper that allows a signal-backed field to behave like a standard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt; inside a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’re adding new fields to an existing form, that’s the perfect place to try it.&lt;/p&gt;

&lt;h2 id=&quot;using-signalformcontrol-to-bridge-signal-forms-and-reactive-forms&quot;&gt;Using SignalFormControl to Bridge Signal Forms and Reactive Forms&lt;/h2&gt;

&lt;p&gt;Suppose we need to add three new fields to the shipping address section: &lt;strong&gt;city&lt;/strong&gt;, &lt;strong&gt;state&lt;/strong&gt;, and &lt;strong&gt;zip&lt;/strong&gt;, and we want these to be signal-backed controls.&lt;/p&gt;

&lt;h3 id=&quot;step-1-add-signalformcontrol-to-the-formgroup-interface&quot;&gt;Step 1: Add SignalFormControl to the FormGroup Interface&lt;/h3&gt;

&lt;p&gt;First, we need to update the form interface to include the new fields.&lt;/p&gt;

&lt;p&gt;Instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt; we’ll use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SignalFormControl&lt;/code&gt; from the Signal Forms compat API:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignalFormControl&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms/signals/compat&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kr&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ProfileForm&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;street&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;city&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignalFormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignalFormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignalFormControl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The group now mixes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SignalFormControl&lt;/code&gt;, that’s the whole point of the bridge.&lt;/p&gt;

&lt;h3 id=&quot;step-2-creating-signalformcontrol-with-signal-based-validation&quot;&gt;Step 2: Creating SignalFormControl with Signal-Based Validation&lt;/h3&gt;

&lt;p&gt;Now let’s create the new signal-backed controls.&lt;/p&gt;

&lt;p&gt;The first parameter is the initial value, the second is the schema callback familiar from Signal Forms where we can add validation:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;city&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignalFormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;City is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now we’ll do the same for the state field:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignalFormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;State is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And one more for the zip field:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;zip&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SignalFormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ZIP code is required&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\d{5}&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;$/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;ZIP code must be 5 digits&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This field also has a pattern validator to ensure the format is 5 digits.&lt;/p&gt;

&lt;h3 id=&quot;step-3-mixing-signalformcontrol-inside-an-existing-formgroup&quot;&gt;Step 3: Mixing SignalFormControl Inside an Existing FormGroup&lt;/h3&gt;

&lt;p&gt;Now we have our signal-based form controls.&lt;/p&gt;

&lt;p&gt;All that’s left is to add them to the form.&lt;/p&gt;

&lt;p&gt;This is pretty easy, we just add them to the shipping address group using these new properties:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ProfileForm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;shippingAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;street&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormControl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Validators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;city&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;city&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt; type, submit handler, and root-level structure stay the same.&lt;/p&gt;

&lt;p&gt;We’ve officially mixed signal-backed controls into a Reactive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt; without changing the form itself.&lt;/p&gt;

&lt;h2 id=&quot;using-the-formfield-directive-with-signalformcontrol&quot;&gt;Using the formField Directive with SignalFormControl&lt;/h2&gt;

&lt;p&gt;Now we need to render these fields in our template.&lt;/p&gt;

&lt;p&gt;We can’t use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formControlName&lt;/code&gt; for these new fields because they’re not plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt; instances.&lt;/p&gt;

&lt;p&gt;They expose a &lt;a href=&quot;https://angular.dev/api/forms/signals/FieldTree&quot; target=&quot;_blank&quot;&gt;fieldTree&lt;/a&gt;, which is the signal-based representation of the control’s state (value, touched, errors, etc.).&lt;/p&gt;

&lt;p&gt;Instead, we need to use the &lt;a href=&quot;https://angular.dev/api/forms/signals/FormField&quot; target=&quot;_blank&quot;&gt;FormField&lt;/a&gt; directive from the Signal Forms API.&lt;/p&gt;

&lt;p&gt;So let’s add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormField&lt;/code&gt; to the component imports:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@angular/forms/signals&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;app-profile-form&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;...,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;imports&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ReactiveFormsModule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;FormField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then over in the template, inside the shipping address section, we’ll use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;formField&lt;/code&gt; directive to connect the input to the signal field tree:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
  City
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;city.fieldTree&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This properly wires up the signal-backed control to the input.&lt;/p&gt;

&lt;p&gt;Then, we’ll use a template variable to check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;touched&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;errors&lt;/code&gt; and iterate over error messages to handle validation:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
  City
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;city.fieldTree&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  @let cityControl = city.fieldTree();
  @if (cityControl.touched() &lt;span class=&quot;err&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; cityControl.errors(); as errors) {
    @for (error of errors; track error) {
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    }
  }
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And that’s it, the City field is now signal-backed inside a Reactive Form.&lt;/p&gt;

&lt;p&gt;Then, we do the same for the State field:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
  State
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;state.fieldTree&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  @let stateControl = state.fieldTree();
  @if (stateControl.touched() &lt;span class=&quot;err&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; stateControl.errors(); as errors) {
    @for (error of errors; track error) {
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    }
  }
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And one more time for the Zip Code field:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
  Zip Code
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;[formField]=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;zip.fieldTree&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  @let zipControl = zip.fieldTree();
  @if (zipControl.touched() &lt;span class=&quot;err&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; zipControl.errors(); as errors) {
    @for (error of errors; track error) {
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    }
  }
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Alright, this is all we need now to intermingle signal form controls with reactive forms.&lt;/p&gt;

&lt;p&gt;Let’s save and try it out!&lt;/p&gt;

&lt;h2 id=&quot;testing-signalformcontrol-inside-a-reactive-form&quot;&gt;Testing SignalFormControl Inside a Reactive Form&lt;/h2&gt;

&lt;p&gt;Now we have our new fields in the address section:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-12/reactive-form-mixed-with-new-signal-form-controls.jpg&quot; alt=&quot;Profile form with the new city, state, and zip fields&quot; width=&quot;1684&quot; height=&quot;854&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;We also have these new properties displaying within our form group object:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-12/reactive-form-with-signal-controls-added-in-value.jpg&quot; alt=&quot;The new city, state, and zip signal-backed fields in the form group object with the new properties&quot; width=&quot;1586&quot; height=&quot;532&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;When we add a value in the City field, the debug panel still reflects everything correctly:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-12/reactive-form-with-signal-controls-updating-real-time.jpg&quot; alt=&quot;The new city, state, and zip signal-backed fields updating in real time within the reactive form group&quot; width=&quot;2348&quot; height=&quot;750&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;From Angular’s perspective, this behaves like a normal &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormControl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But under the hood, this field is powered by signals.&lt;/p&gt;

&lt;p&gt;And when we clear out the value:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-12/reactive-form-with-signal-controls-validation-error.jpg&quot; alt=&quot;The new city signal-backed field with a validation error&quot; width=&quot;1658&quot; height=&quot;728&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;p&gt;A validation error shows, so our validation is working too!&lt;/p&gt;

&lt;p&gt;And once we fill out all of the required fields properly, the form becomes valid and the submit button enables:&lt;/p&gt;

&lt;div&gt;&lt;img src=&quot;/assets/img/content/uploads/2026/02-12/reactive-form-with-signal-controls-valid-submit-button-enabled.jpg&quot; alt=&quot;The new reactive form with signal-backed fields filled out and the form valid with submit button enabled&quot; width=&quot;2284&quot; height=&quot;912&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;&lt;/div&gt;

&lt;h2 id=&quot;how-signalformcontrol-enables-incremental-migration&quot;&gt;How SignalFormControl Enables Incremental Migration&lt;/h2&gt;

&lt;p&gt;In this example we did not:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Rewrite the entire form&lt;/li&gt;
  &lt;li&gt;Replace the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormGroup&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Break existing template bindings&lt;/li&gt;
  &lt;li&gt;Change the submit handler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We adopted Signal Forms incrementally.&lt;/p&gt;

&lt;p&gt;For teams maintaining large Reactive Forms codebases, this is a major unlock.&lt;/p&gt;

&lt;p&gt;It’s the kind of feature that makes gradual migration possible instead of painful.&lt;/p&gt;

&lt;p&gt;If you’re planning a Signal Forms strategy for your app, this is where you start.&lt;/p&gt;

&lt;p&gt;If this helped you, be sure to &lt;a href=&quot;https://www.youtube.com/c/briantreese?sub_confirmation=1&quot;&gt;subscribe&lt;/a&gt; for more deep dives into modern Angular features.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/angular-signal-forms-compat-example&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.2&quot; target=&quot;_blank&quot;&gt;v21.2.0-next.2 Release Notes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms Guide&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://angular.dev/guide/forms/reactive-forms&quot; target=&quot;_blank&quot;&gt;Angular Reactive Forms Guide&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jdoqocy.com/click-101557355-17135603&quot; target=&quot;_blank&quot;&gt;Get a Pluralsight FREE TRIAL HERE!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;W&lt;/span&gt;hat if you could start using &lt;a href=&quot;https://angular.dev/essentials/signal-forms&quot; target=&quot;_blank&quot;&gt;Angular Signal Forms&lt;/a&gt; inside an existing &lt;a href=&quot;https://angular.dev/guide/forms/reactive-forms&quot; target=&quot;_blank&quot;&gt;Reactive Form&lt;/a&gt; without rewriting the entire thing? Well, in &lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.0&quot; target=&quot;_blank&quot;&gt;Angular 21.2-next&lt;/a&gt;, you can! The new &lt;code&gt;SignalFormControl&lt;/code&gt; class acts as a compatibility bridge, letting you mix signal-backed fields into a traditional &lt;code&gt;FormGroup&lt;/code&gt; while keeping your existing form structure intact. This post walks through how it works, how to add it to an existing form, and what changes in the template.&lt;/p&gt;

</description><pubDate>Thu, 12 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-signalformcontrol-reactive-forms-compatibility/</link>
        <guid isPermaLink="true">https://briantree.se/angular-signalformcontrol-reactive-forms-compatibility/</guid><category>Angular</category><category>Angular Forms</category><category>Angular Form Group</category><category>Signal Forms</category><category>Reactive Forms</category><category>Angular Form Control</category><category>Angular 21</category></item><item>
        <title>Angular 21.2 New Feature: Arrow Functions in Templates (With Gotchas)</title>
        <description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;f you&apos;ve ever created a method just to call &lt;code&gt;update()&lt;/code&gt; on a &lt;a href=&quot;https://angular.dev/guide/signals&quot; target=&quot;_blank&quot;&gt;signal&lt;/a&gt;, this one&apos;s for you. Writing signal updates directly in component templates used to be impossible, but as of &lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.0&quot; target=&quot;_blank&quot;&gt;Angular 21.2-next&lt;/a&gt;, arrow functions are now allowed in Angular templates! This change lets you write signal transitions exactly where they happen, making templates more expressive and eliminating unnecessary wrapper methods. But while it&apos;s incredibly powerful, there are a few caveats you absolutely need to understand.&lt;/p&gt;

&lt;div class=&quot;youtube-embed-wrapper&quot;&gt;
	&lt;iframe width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://www.youtube.com/embed/xoCQ6Pyv0C0?rel=1&amp;amp;modestbranding=1&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen=&quot;&quot; loading=&quot;lazy&quot; title=&quot;Angular 21.2 New Feature: Arrow Functions in Templates (With Gotchas)&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;order-summary-demo-built-with-angular-signals&quot;&gt;Order Summary Demo Built with Angular Signals&lt;/h2&gt;

&lt;p&gt;Here’s a basic order summary application that we’ll be using throughout this example:&lt;/p&gt;

&lt;div&gt;
&lt;img src=&quot;/assets/img/content/uploads/2026/02-05/angular-order-summary-signals-demo.jpg&quot; alt=&quot;Screenshot of an Angular order summary application showing quantity controls with increment and decrement buttons, a discount coupon toggle button, a tax rate increase button, and real-time calculated subtotal, discount, tax, and total values&quot; width=&quot;1472&quot; height=&quot;1014&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;We have a quantity control to adjust the quantity, a button to add a discount coupon, and another button to increase the tax rate.&lt;/p&gt;

&lt;p&gt;When we use any of these buttons, we can see the totals at the bottom adjust in real-time:&lt;/p&gt;

&lt;div&gt;
&lt;img src=&quot;/assets/img/content/uploads/2026/02-05/angular-order-summary-totals-updating.gif&quot; alt=&quot;Screenshot highlighting the order summary totals section showing real-time updates: subtotal, discount amount, tax amount, and final total recalculating automatically after user interactions with quantity, discount, or tax controls&quot; width=&quot;1778&quot; height=&quot;646&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Pretty basic stuff, all handled with signals.&lt;/p&gt;

&lt;p&gt;Now let’s look at the code behind all of this.&lt;/p&gt;

&lt;h2 id=&quot;the-old-way-before-arrow-functions-in-templates&quot;&gt;The Old Way: Before Arrow Functions in Templates&lt;/h2&gt;

&lt;p&gt;Let’s start with &lt;a href=&quot;https://github.com/brianmtreese/angular-arrow-functions-templates-example&quot; target=&quot;_blank&quot;&gt;the template&lt;/a&gt; for this component.&lt;/p&gt;

&lt;p&gt;The buttons used to adjust the quantity look like this:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;decrementQty()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;−&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;{{ qty() }}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;incrementQty()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For the decrement button we have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;decrementQty()&lt;/code&gt; function, and for the increment button we have an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;incrementQty()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Then, for the coupon button, same thing, we have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toggleCoupon()&lt;/code&gt; function:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggleCoupon()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  {{ couponOn() ? &apos;Remove 20% coupon&apos; : &apos;Apply 20% coupon&apos; }}
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And, same thing with the tax rate button:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;increaseTax()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+1%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So each button has its own function.&lt;/p&gt;

&lt;p&gt;Now let’s switch over to &lt;a href=&quot;https://github.com/brianmtreese/angular-arrow-functions-templates-example/blob/master/src/app/order-summary/order-summary.component.ts&quot; target=&quot;_blank&quot;&gt;the TypeScript&lt;/a&gt; to see what these functions do.&lt;/p&gt;

&lt;p&gt;Here are all four of these functions:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;incrementQty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;qty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;decrementQty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;qty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;toggleCoupon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;couponOn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;increaseTax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;taxRate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.01&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each of them simply updates a signal value using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update()&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Essentially, they exist for one reason: because we aren’t able to use arrow functions in templates.&lt;/p&gt;

&lt;p&gt;If we were just calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set()&lt;/code&gt;, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update()&lt;/code&gt; requires a function.&lt;/p&gt;

&lt;p&gt;However, now as of Angular 21.2-next.0, we can actually use arrow functions in the template!&lt;/p&gt;

&lt;p&gt;So let’s modernize this!&lt;/p&gt;

&lt;h2 id=&quot;angular-212-using-arrow-functions-directly-in-templates&quot;&gt;Angular 21.2: Using Arrow Functions Directly in Templates&lt;/h2&gt;

&lt;p&gt;First, we can delete all of those wrapper functions since they’re not needed anymore.&lt;/p&gt;

&lt;p&gt;Then, back in the HTML, let’s update these buttons.&lt;/p&gt;

&lt;p&gt;Let’s start with the decrement button.&lt;/p&gt;

&lt;p&gt;Instead of the old function, I’m going to add the signal &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update()&lt;/code&gt; method right here in the template.&lt;/p&gt;

&lt;h4 id=&quot;before&quot;&gt;Before:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;decrementQty()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;−&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after&quot;&gt;After:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;qty.update(n =&amp;gt; n &amp;gt; 0 ? n - 1 : 0)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;−&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We’re passing a function to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update()&lt;/code&gt; method, and that function receives the previous value.&lt;/p&gt;

&lt;p&gt;That’s what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; is. It’s the current quantity.&lt;/p&gt;

&lt;p&gt;Then, we’re returning the new value.&lt;/p&gt;

&lt;p&gt;Now let’s do the increment button.&lt;/p&gt;

&lt;h4 id=&quot;before-1&quot;&gt;Before:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;incrementQty()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after-1&quot;&gt;After:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;qty.update(n =&amp;gt; n + 1)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it. No wrapper method needed.&lt;/p&gt;

&lt;p&gt;Now let’s convert the coupon button.&lt;/p&gt;

&lt;h4 id=&quot;before-2&quot;&gt;Before:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggleCoupon()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  {{ couponOn() ? &apos;Remove 20% coupon&apos; : &apos;Apply 20% coupon&apos; }}
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after-2&quot;&gt;After:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;couponOn.update(v =&amp;gt; !v)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  {{ couponOn() ? &apos;Remove 20% coupon&apos; : &apos;Apply 20% coupon&apos; }}
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Instead of the old method, we use the signal and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update()&lt;/code&gt; method directly.&lt;/p&gt;

&lt;p&gt;Same idea here, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v&lt;/code&gt; is the previous boolean value. We just flip it when the button is clicked.&lt;/p&gt;

&lt;p&gt;Now we just need to update the increase tax button.&lt;/p&gt;

&lt;h4 id=&quot;before-3&quot;&gt;Before:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;increaseTax()&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+1%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after-3&quot;&gt;After:&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;taxRate.update(r =&amp;gt; r + 0.01)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+1%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Instead of the old method again, we just use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update()&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Alright, let’s save and make sure everything still works:&lt;/p&gt;

&lt;div&gt;
&lt;img src=&quot;/assets/img/content/uploads/2026/02-05/angular-arrow-functions-refactor-verification.gif&quot; alt=&quot;Animated demonstration of the order summary application working correctly after refactoring to use arrow functions directly in templates, showing quantity adjustments, discount toggling, and tax rate changes all updating the totals in real-time&quot; width=&quot;1528&quot; height=&quot;1052&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Okay, it all looks the same to start, and when I change the quantity, discount, and tax rate, everything still works.&lt;/p&gt;

&lt;p&gt;Pretty cool right?&lt;/p&gt;

&lt;p&gt;The fact that the only way I could use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update()&lt;/code&gt; function was in the TypeScript has definitely bothered me in the past, so I’m pretty stoked on this update.&lt;/p&gt;

&lt;p&gt;This is the real power here.&lt;/p&gt;

&lt;p&gt;That’s four methods deleted, four fewer things to name, four fewer things to test, and four fewer places for bugs to hide.&lt;/p&gt;

&lt;p&gt;Now, while this is awesome, arrow functions in templates do have some constraints.&lt;/p&gt;

&lt;p&gt;Let’s look at one.&lt;/p&gt;

&lt;h2 id=&quot;angular-template-mistake-accidentally-returning-a-function&quot;&gt;Angular Template Mistake: Accidentally Returning a Function&lt;/h2&gt;

&lt;p&gt;This will probably be a rare mistake to make, but you need to be careful not to accidentally return a function in your event handler like this:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- ❌ Wrong: This compiles but does nothing --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;() =&amp;gt; qty.update(n =&amp;gt; n + 1)&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This compiles… but it does nothing because we’re just returning a function.&lt;/p&gt;

&lt;p&gt;An easy way to avoid this is to only use simple arrow functions in the template.&lt;/p&gt;

&lt;p&gt;If it’s more than 5 to 7-ish lines, it probably belongs in the TypeScript.&lt;/p&gt;

&lt;p&gt;Okay, now let’s look at something you’re more likely to encounter.&lt;/p&gt;

&lt;h2 id=&quot;angular-error-object-literals-must-be-wrapped-in-parentheses&quot;&gt;Angular Error: Object Literals Must Be Wrapped in Parentheses&lt;/h2&gt;

&lt;p&gt;Here, I’ve now changed this example around a little so that the tax rate is now part of a settings object instead of its own signal:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;settings&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;SummarySettings&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;taxRate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.08&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// ... other settings&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And our “total” computed signal now uses this in its calculation too:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;total&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;computed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;discounted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;settings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;taxRate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So now let’s update the tax rate in the template using this settings object signal.&lt;/p&gt;

&lt;p&gt;Right now, our event handler is empty:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+1%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So let’s update the settings signal using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update()&lt;/code&gt; method:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;settings.update(s =&amp;gt; { taxRate: s.taxRate + 0.01 })&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+1%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The tax rate will be set using the current tax rate value and adding 0.01 to it.&lt;/p&gt;

&lt;p&gt;Okay that should be it right?&lt;/p&gt;

&lt;p&gt;Let’s save and try it out:&lt;/p&gt;

&lt;div&gt;
&lt;img src=&quot;/assets/img/content/uploads/2026/02-05/angular-arrow-functions-object-literal-error.jpg&quot; alt=&quot;Screenshot of the error: Object literals must be wrapped in parentheses&quot; width=&quot;2560&quot; height=&quot;1440&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Oops, looks like we have an error.&lt;/p&gt;

&lt;p&gt;And if we read it closely we can see that it’s telling us exactly what the problem is.&lt;/p&gt;

&lt;p&gt;If we’re using an object literal, we need to wrap it with parentheses.&lt;/p&gt;

&lt;p&gt;This is because in JavaScript, curly braces after an arrow indicate a function body, not an object literal.&lt;/p&gt;

&lt;p&gt;Angular templates only allow implicit returns.&lt;/p&gt;

&lt;p&gt;That means the arrow function must evaluate to a single expression.&lt;/p&gt;

&lt;p&gt;So Angular thinks you’re writing a block and blocks aren’t supported here.&lt;/p&gt;

&lt;p&gt;So the fix is simple, we just need to add parentheses.&lt;/p&gt;

&lt;h4 id=&quot;before-error&quot;&gt;Before (Error):&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;settings.update(s =&amp;gt; { taxRate: s.taxRate + 0.01 })&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+1%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after-fixed&quot;&gt;After (Fixed):&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;(click)=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;settings.update(s =&amp;gt; ({ taxRate: s.taxRate + 0.01 }))&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;+1%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now let’s look at another constraint.&lt;/p&gt;

&lt;h2 id=&quot;angular-pipes-and-arrow-functions-what-works-and-what-breaks&quot;&gt;Angular Pipes and Arrow Functions: What Works and What Breaks&lt;/h2&gt;

&lt;p&gt;We have to be careful when using pipes with arrow functions.&lt;/p&gt;

&lt;p&gt;We’re going to calculate shipping cost based on distance.&lt;/p&gt;

&lt;p&gt;Let’s add a new row:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;row&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Shipping (100 miles)&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’ll give it a label denoting that it’s based on 100 miles.&lt;/p&gt;

&lt;p&gt;We’ll just need to imagine that this 100 mile distance would be a dynamic value that could be anything.&lt;/p&gt;

&lt;p&gt;It could be 5 miles, 500 miles, 2000 miles, it could be anything but here we’ll just be using a static value of 100.&lt;/p&gt;

&lt;p&gt;Okay, now I’ll add an arrow function to calculate this value:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;row&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Shipping (100 miles)&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&amp;gt;&lt;/span&gt;{{ ((dist, rate) =&amp;gt; dist * rate | currency)(100, 0.05) }}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We have a distance (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dist&lt;/code&gt;) and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rate&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;Then we return the distance times the rate.&lt;/p&gt;

&lt;p&gt;We also use the currency pipe to format the value.&lt;/p&gt;

&lt;p&gt;Next, we immediately call this function passing it our static 100 mile value and a rate of 0.05.&lt;/p&gt;

&lt;p&gt;Okay, now let’s save and try this out:&lt;/p&gt;

&lt;div&gt;
&lt;img src=&quot;/assets/img/content/uploads/2026/02-05/angular-arrow-functions-pipes-error.jpg&quot; alt=&quot;Screenshot of the error: Pipes are Angular template syntax, not JavaScript&quot; width=&quot;2560&quot; height=&quot;1440&quot; style=&quot;width: 100%; height: auto;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Oops, we’ve got an error again now. But why?&lt;/p&gt;

&lt;p&gt;Well, this time it’s because pipes are Angular template syntax, not JavaScript.&lt;/p&gt;

&lt;p&gt;The arrow function body must be valid JavaScript, and the pipe operator isn’t JavaScript.&lt;/p&gt;

&lt;p&gt;So the rule is you cannot use pipes inside arrow bodies.&lt;/p&gt;

&lt;p&gt;You must instead apply the pipe to the result of the arrow function.&lt;/p&gt;

&lt;p&gt;This means, we just need to move it outside:&lt;/p&gt;

&lt;h4 id=&quot;before-error-1&quot;&gt;Before (Error):&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;span&amp;gt;&lt;/span&gt;{{ ((dist, rate) =&amp;gt; dist * rate | currency)(100, 0.05) }}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;after-fixed-1&quot;&gt;After (Fixed):&lt;/h4&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;span&amp;gt;&lt;/span&gt;{{ ((dist, rate) =&amp;gt; dist * rate)(100, 0.05) | currency }}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So…&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;We can only have implicit returns with arrow functions in the template.&lt;/li&gt;
  &lt;li&gt;Object literals must be wrapped in parentheses&lt;/li&gt;
  &lt;li&gt;Pipes cannot live inside arrow function bodies&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;when-to-use-arrow-functions-in-angular-templates&quot;&gt;When to Use Arrow Functions in Angular Templates&lt;/h2&gt;

&lt;p&gt;This is one of those changes that seems small until you start removing code.&lt;/p&gt;

&lt;p&gt;It lets us write signal transitions exactly where they happen.&lt;/p&gt;

&lt;p&gt;It makes our templates more expressive and our overall logic simpler.&lt;/p&gt;

&lt;p&gt;But it also forces us to understand the difference between Angular template syntax and JavaScript expressions.&lt;/p&gt;

&lt;p&gt;If you’re building modern Angular apps using signals, this change definitely matters.&lt;/p&gt;

&lt;p&gt;If this helped you, be sure to &lt;a href=&quot;https://www.youtube.com/c/briantreese?sub_confirmation=1&quot;&gt;subscribe&lt;/a&gt; and leave a comment, it really helps other Angular developers find this content.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brianmtreese/angular-template-arrow-functions-demo&quot; target=&quot;_blank&quot;&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions&quot; target=&quot;_blank&quot;&gt;MDN Arrow function expressions documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.0&quot; target=&quot;_blank&quot;&gt;Angular 21.2.0-next.0 release notes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pluralsight.com/courses/angular-styling-applications&quot; target=&quot;_blank&quot;&gt;My course “Angular: Styling Applications”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection&quot; target=&quot;_blank&quot;&gt;My course “Angular in Practice: Zoneless Change Detection”&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jdoqocy.com/click-101557355-17135603&quot; target=&quot;_blank&quot;&gt;Get a Pluralsight FREE TRIAL HERE!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><description>&lt;p class=&quot;intro&quot;&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;f you&apos;ve ever created a method just to call &lt;code&gt;update()&lt;/code&gt; on a &lt;a href=&quot;https://angular.dev/guide/signals&quot; target=&quot;_blank&quot;&gt;signal&lt;/a&gt;, this one&apos;s for you. Writing signal updates directly in component templates used to be impossible, but as of &lt;a href=&quot;https://github.com/angular/angular/releases/tag/v21.2.0-next.0&quot; target=&quot;_blank&quot;&gt;Angular 21.2-next&lt;/a&gt;, arrow functions are now allowed in Angular templates! This change lets you write signal transitions exactly where they happen, making templates more expressive and eliminating unnecessary wrapper methods. But while it&apos;s incredibly powerful, there are a few caveats you absolutely need to understand.&lt;/p&gt;

</description><pubDate>Thu, 05 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://briantree.se/angular-arrow-functions-in-templates/</link>
        <guid isPermaLink="true">https://briantree.se/angular-arrow-functions-in-templates/</guid><category>Angular</category><category>Angular Templates</category><category>Angular Signals</category><category>Angular 21</category><category>TypeScript</category><category>Template Syntax</category></item></channel>
</rss>
