<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Brian Treese</title>
    <description>The latest articles on DEV Community by Brian Treese (@brianmtreese).</description>
    <link>https://dev.to/brianmtreese</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F324503%2F0e7b14ea-502c-4967-a707-62f8a8c9a61e.jpg</url>
      <title>DEV Community: Brian Treese</title>
      <link>https://dev.to/brianmtreese</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brianmtreese"/>
    <language>en</language>
    <item>
      <title>Angular’s New injectAsync() API Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 08 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angulars-new-injectasync-api-explained-41fh</link>
      <guid>https://dev.to/brianmtreese/angulars-new-injectasync-api-explained-41fh</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular v22 just made lazy-loading services much simpler. In this post, we'll explore how to leverage the new &lt;a href="https://github.com/angular/angular/commit/444b024d49725afc8b40aec67cfdb63a1f7f23ea#diff-2e2d6a33b5784aebcbbedeb04abba424b55621fe9ec56d4e44618fdf3d7eedb1" rel="noopener noreferrer"&gt;injectAsync()&lt;/a&gt; API to reduce your main bundle size and replace awkward lazy-loading workarounds. We’ll compare the older manual lazy-loading approach with Angular v22’s new &lt;code&gt;injectAsync()&lt;/code&gt; API and see why the new pattern feels simpler and easier to reason about.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/59J8c3ZBOBQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Libraries Loading Too Early
&lt;/h2&gt;

&lt;p&gt;When building Angular applications, it's common to include third-party libraries for various functionalities. &lt;/p&gt;

&lt;p&gt;However, if these libraries are not loaded efficiently, they can increase your initial bundle size, leading to slower application startup times. &lt;/p&gt;

&lt;p&gt;In our &lt;a href="https://github.com/brianmtreese/angular-inject-async" rel="noopener noreferrer"&gt;demo application&lt;/a&gt;, we're using &lt;a href="https://highlightjs.org/" rel="noopener noreferrer"&gt;highlight.js&lt;/a&gt; and &lt;a href="https://marked.js.org/" rel="noopener noreferrer"&gt;Marked&lt;/a&gt; for markdown processing and syntax highlighting.&lt;/p&gt;

&lt;p&gt;In this case, the &lt;a href="https://github.com/brianmtreese/angular-inject-async/tree/main/src/app/post-editor" rel="noopener noreferrer"&gt;post-editor component&lt;/a&gt; itself is needed immediately, but the markdown-processing dependency isn’t.&lt;/p&gt;

&lt;p&gt;Even though the feature might not be immediately needed by the user, these libraries are loaded upfront, contributing to a 17.2kb main bundle:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Fbundle-size.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Fbundle-size.jpg" alt="The bundle size of the application" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Fexternal-libraries.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Fexternal-libraries.jpg" alt="The external libraries that are loaded upfront" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This eager loading means users are paying for code they might never use, impacting performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Markdown Service and its Eager Usage
&lt;/h2&gt;

&lt;p&gt;Our demo application features a &lt;a href="https://github.com/brianmtreese/angular-inject-async/blob/main/src/app/markdown.service.ts" rel="noopener noreferrer"&gt;MarkdownService&lt;/a&gt; responsible for converting markdown content into HTML with syntax highlighting. &lt;/p&gt;

&lt;p&gt;This service leverages marked, marked-highlight, and highlight.js to achieve this functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;hljs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;highlight.js/lib/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Marked&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;markedHighlight&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marked-highlight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MarkdownService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Marked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;markedHighlight&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;emptyLangClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hljs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;langPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hljs language-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hljs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;plaintext&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hljs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core issue arises from how this service is consumed. &lt;/p&gt;

&lt;p&gt;Initially, the &lt;a href="https://github.com/brianmtreese/angular-inject-async/blob/main/src/app/post-editor/post-editor.component.ts" rel="noopener noreferrer"&gt;post-editor component&lt;/a&gt; directly injects the &lt;code&gt;MarkdownService&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MarkdownService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../markdown.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-post-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./post-editor.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./post-editor.component.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostEditorComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;previewHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;markdownService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MarkdownService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markdownService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the &lt;code&gt;MarkdownService&lt;/code&gt; is statically imported and injected into &lt;code&gt;PostEditorComponent&lt;/code&gt;, Angular includes it and all its dependencies (&lt;code&gt;marked&lt;/code&gt;, &lt;code&gt;highlight.js&lt;/code&gt;) in the initial application bundle. &lt;/p&gt;

&lt;p&gt;This happens regardless of whether the user has interacted with the markdown preview feature, leading to unnecessary upfront loading and a larger initial bundle size. &lt;/p&gt;

&lt;p&gt;This is the "problem" we aim to solve with lazy loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Way: Manually Lazy Loading an Angular Service
&lt;/h2&gt;

&lt;p&gt;Historically, addressing eager loading for services involved a fair amount of manual setup. &lt;/p&gt;

&lt;p&gt;To lazy load the &lt;code&gt;MarkdownService&lt;/code&gt; and its dependencies, we would typically inject Angular's &lt;code&gt;Injector&lt;/code&gt; and dynamically import the service.&lt;/p&gt;

&lt;p&gt;It might look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Injector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-post-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./post-editor.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./post-editor.component.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostEditorComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;previewHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;injector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Injector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;markdownServicePromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MarkdownService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markdownServicePromise&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../markdown.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;injector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MarkdownService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markdownService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markdownServicePromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdownService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach solves the problem.&lt;/p&gt;

&lt;p&gt;The bundle size is reduced, and &lt;code&gt;highlight.js&lt;/code&gt; and &lt;code&gt;marked&lt;/code&gt; are no longer part of the initial page load:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Fbundle-size-lazy-loaded.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Fbundle-size-lazy-loaded.jpg" alt="The bundle size of the application after lazy loading" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Finitlial-dependencies.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Finitlial-dependencies.jpg" alt="The initial dependencies that are loaded upfront" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;They are now dynamically loaded only when the &lt;code&gt;preview()&lt;/code&gt; method is called:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Fexternal-libraries-lazy-loaded.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-07%2Fexternal-libraries-lazy-loaded.jpg" alt="The external libraries that are loaded lazily" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, this manual lazy loading introduces a lot of setup, making it less ergonomic and harder to maintain.&lt;/p&gt;




&lt;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" " width="800" height="416"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The New Way: Replacing Boilerplate with injectAsync()
&lt;/h2&gt;

&lt;p&gt;Angular v22 introduces &lt;code&gt;injectAsync()&lt;/code&gt;, a new API that simplifies lazy loading services. &lt;/p&gt;

&lt;p&gt;This function handles much of the orchestration we previously had to manage ourselves.&lt;/p&gt;

&lt;p&gt;To refactor our post-editor component using &lt;code&gt;injectAsync()&lt;/code&gt;, we can remove the &lt;code&gt;Injector&lt;/code&gt; and the &lt;code&gt;markdownServicePromise&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;injectAsync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onIdle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-post-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./post-editor.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./post-editor.component.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostEditorComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;previewHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;markdownService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;injectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../markdown.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MarkdownService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdownService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;injectAsync()&lt;/code&gt;, we pass a loader function that dynamically imports the service. &lt;/p&gt;

&lt;p&gt;Angular automatically captures the current injection context, resolves the service through dependency injection, and caches the result.&lt;/p&gt;

&lt;p&gt;This makes the implementation simpler and easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefetch Lazy Dependencies with onIdle
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;injectAsync()&lt;/code&gt; also offers a &lt;code&gt;prefetch&lt;/code&gt; option, allowing us to load dependencies early, but without blocking the initial bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;onIdle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;markdownService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;injectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../markdown.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MarkdownService&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onIdle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By setting &lt;code&gt;prefetch: onIdle&lt;/code&gt;, Angular will begin loading the dependency quietly in the background once the browser becomes idle.&lt;/p&gt;

&lt;p&gt;This means the feature stays out of the initial bundle, but the user might never notice a loading delay because the service is already prefetched by the time they need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Result
&lt;/h2&gt;

&lt;p&gt;The application continues to function exactly as before, but with an optimized loading strategy. &lt;/p&gt;

&lt;p&gt;The initial bundle size is smaller, and the markdown libraries are lazy-loaded, either on demand or prefetched during browser idle time. &lt;/p&gt;

&lt;p&gt;This makes the pattern much more practical for real-world applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Ahead of Angular's Next Shift
&lt;/h2&gt;

&lt;p&gt;Angular’s newest APIs are changing the way we build. &lt;/p&gt;

&lt;p&gt;If you’re ready to go deeper with one of the biggest shifts in modern Angular, my Signal Forms course will help you get comfortable with the new forms model.&lt;/p&gt;

&lt;p&gt;You can access it either directly or through YouTube membership, whichever works best for you: &lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Buy the course&lt;/a&gt;&lt;br&gt;
👉 &lt;a href="https://www.youtube.com/channel/UCdPhLDznZzUeEtshDUe0R_A/join" rel="noopener noreferrer"&gt;Get it with YouTube membership&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-inject-async" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/444b024d49725afc8b40aec67cfdb63a1f7f23ea" rel="noopener noreferrer"&gt;Angular v22 &lt;code&gt;injectAsync&lt;/code&gt; commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/di?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Dependency Injection Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import" rel="noopener noreferrer"&gt;Dynamic Imports (MDN)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Angular 22 @Service vs @Injectable (What You Need to Know)"</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 01 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-22-service-vs-injectable-what-you-need-to-know-3i9l</link>
      <guid>https://dev.to/brianmtreese/angular-22-service-vs-injectable-what-you-need-to-know-3i9l</guid>
      <description>&lt;p&gt;&lt;span&gt;E&lt;/span&gt;very Angular developer is familiar with &lt;code&gt;@Injectable({ providedIn: 'root' })&lt;/code&gt; for declaring services. While powerful, the &lt;a href="https://angular.dev/api/core/Injectable?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;@Injectable&lt;/a&gt; decorator supports many advanced configurations that are rarely used in typical application services. Angular 22 introduces a new &lt;a href="https://github.com/angular/angular/commit/8f3d0b9d97424e058eb7bce57d80833fb68dec4a" rel="noopener noreferrer"&gt;@Service&lt;/a&gt; decorator designed to simplify service declaration for the most common use cases. This post will explore how it streamlines service creation and even enforces modern dependency injection patterns.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/59J8c3ZBOBQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Version Disclaimer
&lt;/h2&gt;

&lt;p&gt;This API is currently in pre-release, so some details may change before the final Angular 22 release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Overly Complex Service Declaration
&lt;/h2&gt;

&lt;p&gt;For many years, declaring a service in Angular meant using the &lt;code&gt;@Injectable&lt;/code&gt; decorator, often with the &lt;code&gt;providedIn: 'root'&lt;/code&gt; option.&lt;/p&gt;

&lt;p&gt;This pattern, while effective, includes configuration options that are seldom needed for straightforward application services.&lt;/p&gt;

&lt;p&gt;Let's look at a very simple example of a service that fetches user data and posts from an API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./models&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://jsonplaceholder.typicode.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;providedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;selectedUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/users`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/posts?userId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code works perfectly fine in current Angular versions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;@Injectable&lt;/code&gt; decorator registers the service as a singleton in Angular's dependency injection system, making the same instance available wherever &lt;code&gt;PostsService&lt;/code&gt; is injected. &lt;/p&gt;

&lt;h2&gt;
  
  
  The New Way: Angular 22 &lt;code&gt;@Service()&lt;/code&gt; and &lt;code&gt;inject()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Angular 22 introduces the &lt;code&gt;@Service()&lt;/code&gt; decorator as a simpler, more opinionated alternative to &lt;code&gt;@Injectable&lt;/code&gt; for common service patterns. &lt;/p&gt;

&lt;p&gt;It’s intended for most app-level services, not a full replacement for every use case.&lt;/p&gt;

&lt;p&gt;It assumes the most frequent scenario: a root-provided singleton service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Replacing &lt;code&gt;@Injectable&lt;/code&gt; with &lt;code&gt;@Service&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Swapping from &lt;code&gt;@Injectable&lt;/code&gt; to &lt;code&gt;@Service&lt;/code&gt; is straightforward. &lt;/p&gt;

&lt;p&gt;We simply replace the decorator and remove the &lt;code&gt;providedIn: 'root'&lt;/code&gt; option, as it's the default behavior for &lt;code&gt;@Service()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Note: Injectable is removed&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./models&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://jsonplaceholder.typicode.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// ← providedIn: 'root' by default, no config needed&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;selectedUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/users`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectedUserId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/posts?userId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this change, the &lt;code&gt;PostsService&lt;/code&gt; continues to function identically.&lt;/p&gt;

&lt;p&gt;So, &lt;code&gt;@Service()&lt;/code&gt; provides the same root-level singleton behavior without the explicit configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Enforcing &lt;code&gt;inject()&lt;/code&gt; for Dependencies
&lt;/h3&gt;

&lt;p&gt;One of the significant changes with &lt;code&gt;@Service()&lt;/code&gt; is that it does not allow constructor injection. &lt;/p&gt;

&lt;p&gt;If you attempt to inject a dependency via the constructor while using &lt;code&gt;@Service()&lt;/code&gt;, Angular will throw an error. &lt;/p&gt;

&lt;p&gt;This is an intentional guardrail, pushing developers towards the modern &lt;a href="https://angular.dev/api/core/inject?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;inject()&lt;/a&gt; function for dependency resolution.&lt;/p&gt;

&lt;p&gt;Consider our &lt;code&gt;PostsService&lt;/code&gt; with &lt;code&gt;HttpClient&lt;/code&gt; injected in the constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// This will cause an error with @Service()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attempting to run this will result in a compilation error:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-30%2Fservice-constructor-error.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-30%2Fservice-constructor-error.jpg" alt="Compilation error caused by constructor injection in an Angular Service class" width="800" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The solution is to use the &lt;code&gt;inject()&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Note: inject is imported&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;httpResource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Using inject() for dependency&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pushes services toward a more functional and consistent dependency injection style, aligning with how modern Angular APIs are designed.&lt;/p&gt;




&lt;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" " width="800" height="416"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Component-Scoped Services with &lt;code&gt;autoProvided: false&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;While &lt;code&gt;@Service()&lt;/code&gt; defaults to root-level provision, it also offers a powerful option for explicit service scoping: &lt;code&gt;autoProvided: false&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This is particularly useful for services that manage UI-specific state and should have their lifetime tied to a particular component.&lt;/p&gt;

&lt;p&gt;Let's look at a &lt;code&gt;DraftPostService&lt;/code&gt; that manages the state for a draft form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;autoProvided&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// I'll manage where this lives&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DraftPostService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By setting &lt;code&gt;autoProvided: false&lt;/code&gt;, we explicitly state that this service should not be automatically provided in the root injector. &lt;/p&gt;

&lt;p&gt;Instead, its provision becomes the responsibility of the consumer. &lt;/p&gt;

&lt;p&gt;This is ideal for UI state that should not persist across navigation or be globally accessible.&lt;/p&gt;

&lt;p&gt;If we try to use this service in a component without providing it, we'll encounter a &lt;code&gt;NullInjectorError&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-30%2Fnull-injector-error.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-30%2Fnull-injector-error.jpg" alt="Angular NullInjectorError showing no provider found for DraftPostService" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This occurs because Angular's DI system cannot find a provider for &lt;code&gt;DraftPostService&lt;/code&gt; in the injector tree. &lt;/p&gt;

&lt;p&gt;To fix this, we need to explicitly provide the service at the component level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DraftPostService&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./draft-post.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-draft-panel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./draft-panel.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./draft-panel.component.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DraftPostService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// Service provided here&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DraftPanelComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DraftPostService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the &lt;code&gt;DraftPostService&lt;/code&gt;'s lifetime is directly tied to the &lt;code&gt;DraftPanelComponent&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;When the component is created, the service is instantiated. &lt;/p&gt;

&lt;p&gt;When it’s destroyed, the service is destroyed as well.&lt;/p&gt;

&lt;p&gt;This makes the intent of the service's scope explicit and clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Result
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;@Service()&lt;/code&gt; decorator in Angular 22 offers a more streamlined and opinionated way to declare services, especially for the most common use cases. &lt;/p&gt;

&lt;p&gt;It simplifies things, encourages the use of the &lt;code&gt;inject()&lt;/code&gt; function for dependencies, and provides a clear mechanism for defining component-scoped services with &lt;code&gt;autoProvided: false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This isn't just about new syntax, it's about making the intent of your services more explicit and guiding developers toward modern Angular patterns. &lt;/p&gt;

&lt;p&gt;By embracing &lt;code&gt;@Service()&lt;/code&gt;, you can write cleaner, more maintainable code and better communicate the intended lifecycle and scope of your application's services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Ahead of Angular's Next Shift
&lt;/h2&gt;

&lt;p&gt;And speaking of modern patterns, if you’re trying to go deeper on those, especially things like signals and modern forms, I’ve got a new course available on Signal Forms that walks through how all of this fits together in real apps.&lt;/p&gt;

&lt;p&gt;You can access it either directly or through YouTube membership, whichever works best for you: &lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Buy the course&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://www.youtube.com/channel/UCdPhLDznZzUeEtshDUe0R_A/join" rel="noopener noreferrer"&gt;Get it with YouTube membership&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-service-decorator-example" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/8f3d0b9d97424e058eb7bce57d80833fb68dec4a" rel="noopener noreferrer"&gt;Angular v22 &lt;code&gt;@Service&lt;/code&gt; PR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/common/http/httpResource?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;httpResource API docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>This completely changed my Angular development workflow</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 24 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/this-completely-changed-my-angular-development-workflow-3emp</link>
      <guid>https://dev.to/brianmtreese/this-completely-changed-my-angular-development-workflow-3emp</guid>
      <description>&lt;p&gt;&lt;span&gt;Y&lt;/span&gt;ou ever use AI to generate Angular code that's &lt;em&gt;almost&lt;/em&gt; right, but not quite? You end up renaming things, swapping decorators for signals, and rewriting &lt;code&gt;*ngIf&lt;/code&gt; to &lt;code&gt;@if&lt;/code&gt; every single time. Well, in this post, I'll show you how to use &lt;a href="https://cursor.com/docs/skills" rel="noopener noreferrer"&gt;Cursor Skills&lt;/a&gt; to encode those fixes once and automate your Angular workflow for the whole team.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/V1MM8Sejuak"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Fixing "Almost Right" Code
&lt;/h2&gt;

&lt;p&gt;When we use AI to generate Angular code, it often defaults to generic patterns. &lt;/p&gt;

&lt;p&gt;We end up spending time manually refactoring it to match our specific architecture or modern Angular conventions like signals and standalone components.&lt;/p&gt;

&lt;p&gt;Instead of repeating these manual fixes every time, we can use Cursor Skills to tell the AI exactly how we want our code built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Way: Manual Refactoring
&lt;/h2&gt;

&lt;p&gt;Before Cursor Skills, we'd ask the AI for a component and get back something that maybe looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CommonModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-hello-world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;standalone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CommonModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;section class="hello-world"&amp;gt;
      &amp;lt;h2&amp;gt;Hello, world&amp;lt;/h2&amp;gt;
      &amp;lt;p *ngIf="todayDate"&amp;gt;{{ todayDate }}&amp;lt;/p&amp;gt;
      &amp;lt;button type="button" (click)="onClick()"&amp;gt;Update time&amp;lt;/button&amp;gt;
    &amp;lt;/section&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloWorldComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;todayDate&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;updateTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's almost right, but every single line needs a touch-up: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drop &lt;code&gt;standalone: true&lt;/code&gt; (it's the default now)&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;CommonModule&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Swap &lt;code&gt;@Input()&lt;/code&gt; / &lt;code&gt;@Output()&lt;/code&gt; for &lt;code&gt;input()&lt;/code&gt; / &lt;code&gt;output()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;And replace &lt;code&gt;*ngIf&lt;/code&gt; with &lt;code&gt;@if&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;And add &lt;code&gt;OnPush&lt;/code&gt; change detection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing that by hand every time is slow, inconsistent, and worst of all, a guaranteed source of drift across a team. &lt;/p&gt;

&lt;p&gt;Of course we can continue running the old component generation schematic but it's not as flexible as using AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Way: Project-Level Cursor Skills
&lt;/h2&gt;

&lt;p&gt;Cursor Skills allow us to define reusable workflows that are committed directly into our source code. &lt;/p&gt;

&lt;p&gt;This ensures every developer on the team follows the same standards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Setting Up the Skills Directory
&lt;/h3&gt;

&lt;p&gt;We create a &lt;code&gt;.cursor/skills&lt;/code&gt; directory in our project root. &lt;/p&gt;

&lt;p&gt;Inside, we add a folder for each skill. &lt;/p&gt;

&lt;p&gt;In this case we'll create an &lt;code&gt;angular-component-generator&lt;/code&gt; folder. &lt;/p&gt;

&lt;p&gt;Inside this folder, we then need to add a &lt;code&gt;SKILL.md&lt;/code&gt; file inside it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-folder-structure.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-folder-structure.jpg" alt="The folder structure showing the .cursor/skills directory and the SKILL.md file." width="684" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's important to note that the name "SKILL" in this file must be capitalized for Cursor to recognize it as a skill.&lt;/p&gt;

&lt;p&gt;This file is just Markdown with a bit of frontmatter. &lt;/p&gt;

&lt;p&gt;The frontmatter tells Cursor when to use the skill, and the body is the "training material", the conventions, a template, and an example.&lt;/p&gt;

&lt;p&gt;Here's the shape of our component-generator skill (with the full template omitted for brevity):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;angular-component-generator&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generates&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;modern&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Angular&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;standalone&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;component&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;using&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;signals,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OnPush&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;change&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;detection,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;native&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;control&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;flow,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;latest&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Angular&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;conventions.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;asks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;create,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;scaffold,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;generate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Angular&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;component."&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Angular Component Generator&lt;/span&gt;

Creates a single, modern Angular component file that follows current Angular (v20+) conventions.

&lt;span class="gu"&gt;## Conventions to Follow&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Standalone**&lt;/span&gt; — do NOT set &lt;span class="sb"&gt;`standalone: true`&lt;/span&gt; (it's the default in v20+)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Change detection**&lt;/span&gt; — always set &lt;span class="sb"&gt;`changeDetection: ChangeDetectionStrategy.OnPush`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Inputs / outputs**&lt;/span&gt; — use &lt;span class="sb"&gt;`input()`&lt;/span&gt; and &lt;span class="sb"&gt;`output()`&lt;/span&gt; functions, not decorators
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**State**&lt;/span&gt; — use &lt;span class="sb"&gt;`signal()`&lt;/span&gt; for local state, &lt;span class="sb"&gt;`computed()`&lt;/span&gt; for derived state
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Templates**&lt;/span&gt; — prefer inline &lt;span class="sb"&gt;`template`&lt;/span&gt;; use &lt;span class="sb"&gt;`@if`&lt;/span&gt; / &lt;span class="sb"&gt;`@for`&lt;/span&gt; / &lt;span class="sb"&gt;`@switch`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Injection**&lt;/span&gt; — use the &lt;span class="sb"&gt;`inject()`&lt;/span&gt; function, not constructor injection
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Selector**&lt;/span&gt; — kebab-case with an &lt;span class="sb"&gt;`app-`&lt;/span&gt; prefix
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Class name**&lt;/span&gt; — PascalCase ending in &lt;span class="sb"&gt;`Component`&lt;/span&gt;

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Confirm the component name with the user (if not provided).
&lt;span class="p"&gt;2.&lt;/span&gt; Generate the file using the template below.
&lt;span class="p"&gt;3.&lt;/span&gt; Only split into separate &lt;span class="sb"&gt;`.html`&lt;/span&gt; / &lt;span class="sb"&gt;`.css`&lt;/span&gt; files if they're large enough to justify it.

&lt;span class="gu"&gt;## Template&lt;/span&gt;

// ... a reference template the AI uses as a starting point ...

&lt;span class="gu"&gt;## Don'ts&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Don't add &lt;span class="sb"&gt;`standalone: true`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Don't use &lt;span class="sb"&gt;`NgModule`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Don't use &lt;span class="sb"&gt;`@Input()`&lt;/span&gt; / &lt;span class="sb"&gt;`@Output()`&lt;/span&gt; decorators
&lt;span class="p"&gt;-&lt;/span&gt; Don't use &lt;span class="sb"&gt;`*ngIf`&lt;/span&gt; / &lt;span class="sb"&gt;`*ngFor`&lt;/span&gt; / &lt;span class="sb"&gt;`*ngSwitch`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Don't import &lt;span class="sb"&gt;`CommonModule`&lt;/span&gt; just to use control flow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full version is in the &lt;a href="https://github.com/brianmtreese/create-angular-skills-cursor" rel="noopener noreferrer"&gt;project repo&lt;/a&gt;, but notice the pattern: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Describe the conventions&lt;/li&gt;
&lt;li&gt;Provide a template&lt;/li&gt;
&lt;li&gt;And list the things you never want to see&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last section is the one that makes the biggest difference in practice. &lt;/p&gt;

&lt;p&gt;It directly counters the AI's default habits.&lt;/p&gt;




&lt;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" " width="800" height="416"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Running the Skill
&lt;/h3&gt;

&lt;p&gt;To use the skill, we open a new chat and invoke it by typing a forward slash followed by the skill name:&lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-slash-command.jpg" alt="Typing the /angular-component-generator slash command in a new Cursor chat." width="800" height="421"&gt;

&lt;p&gt;Cursor reads our &lt;code&gt;SKILL.md&lt;/code&gt; and asks for anything missing.&lt;/p&gt;

&lt;p&gt;In this case it looks like we need to provide some more information:&lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-clarifying-questions.jpg" alt="Cursor asking follow-up questions for the component name, inputs, and outputs." width="800" height="709"&gt;

&lt;p&gt;So here's what I added:&lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-user-response.jpg" alt="The user's reply with the hello-world component details: todayDate input and updateTime output." width="780" height="392"&gt;

&lt;p&gt;Then, after it does its work, we've got a new component:&lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-generated-component.jpg" alt="The generated HelloWorldComponent file open in the editor after the skill runs." width="762" height="378"&gt;

&lt;p&gt;Here's what the generated code looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-hello-world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;changeDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OnPush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;section class="hello-world"&amp;gt;
      &amp;lt;h2&amp;gt;Hello, world&amp;lt;/h2&amp;gt;
      &amp;lt;p class="hello-world__date"&amp;gt;{% raw %}{{ todayDate() }}{% endraw %}&amp;lt;/p&amp;gt;
      &amp;lt;button type="button" (click)="emitUpdateTime()"&amp;gt;Update time&amp;lt;/button&amp;gt;
    &amp;lt;/section&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    .hello-world {
      display: block;
    }

    .hello-world__date {
      margin: 0.5rem 0;
    }
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloWorldComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;todayDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;updateTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;emitUpdateTime&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to the generic version from earlier: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;standalone: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;CommonModule&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No decorators&lt;/li&gt;
&lt;li&gt;And no &lt;code&gt;*ngIf&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exactly the code we would have spent time hand-editing, or following up with AI, generated for us in one shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating Unit Tests
&lt;/h2&gt;

&lt;p&gt;And skills compose nicely. &lt;/p&gt;

&lt;p&gt;Once we have one for generating components, the obvious next move is a matching skill for generating their tests. &lt;/p&gt;

&lt;p&gt;For this we create a second folder, &lt;code&gt;add-component-unit-tests&lt;/code&gt;, with its own &lt;code&gt;SKILL.md&lt;/code&gt; that encodes how our team writes specs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-add-component-unit-tests-folder.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-add-component-unit-tests-folder.jpg" alt="The .cursor/skills directory with the add-component-unit-tests folder and its SKILL.md file alongside the component generator skill." width="796" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's what the code for that skill might look like (the spec template is abbreviated below for brevity):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add-component-unit-tests&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Evaluates an Angular standalone component and generates unit tests that cover its inputs, outputs, signals, computed values, methods, and template behavior following unit-testing best practices. Use when the user asks to add, generate, scaffold, or write unit tests for an Angular component.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Add Angular Component Unit Tests&lt;/span&gt;

Creates a &lt;span class="sb"&gt;`.component.spec.ts`&lt;/span&gt; file next to an Angular component that covers its public API and rendered behavior using Angular's &lt;span class="sb"&gt;`TestBed`&lt;/span&gt; and Jasmine.

&lt;span class="gu"&gt;## Assumptions&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Angular v20+ standalone components (no &lt;span class="sb"&gt;`NgModule`&lt;/span&gt;, no &lt;span class="sb"&gt;`standalone: true`&lt;/span&gt;).
&lt;span class="p"&gt;-&lt;/span&gt; Inputs/outputs declared with &lt;span class="sb"&gt;`input()`&lt;/span&gt; / &lt;span class="sb"&gt;`input.required()`&lt;/span&gt; / &lt;span class="sb"&gt;`output()`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; State with &lt;span class="sb"&gt;`signal()`&lt;/span&gt; and derived state with &lt;span class="sb"&gt;`computed()`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Native control flow (&lt;span class="sb"&gt;`@if`&lt;/span&gt;, &lt;span class="sb"&gt;`@for`&lt;/span&gt;, &lt;span class="sb"&gt;`@switch`&lt;/span&gt;) in templates.
&lt;span class="p"&gt;-&lt;/span&gt; Host bindings/listeners declared via the &lt;span class="sb"&gt;`host`&lt;/span&gt; object.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`ChangeDetectionStrategy.OnPush`&lt;/span&gt;.

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Read the component file**&lt;/span&gt; to build an inventory of inputs, outputs, signals, computed values, methods, host bindings, template elements, and injected dependencies.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Derive the test plan**&lt;/span&gt; using the Test Plan Heuristics below. Briefly list the cases you will cover before writing code.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Write the spec file**&lt;/span&gt; at &lt;span class="sb"&gt;`&amp;lt;folder&amp;gt;/&amp;lt;name&amp;gt;.component.spec.ts`&lt;/span&gt; using the Spec Template.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Follow the Best Practices**&lt;/span&gt; — one behavior per test, arrange-act-assert, no snapshot tests for signals, no testing of private implementation details.
&lt;span class="p"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Do not modify the component**&lt;/span&gt; to make it testable unless the user asks. If something is genuinely untestable, flag it instead of refactoring silently.

&lt;span class="gu"&gt;## Test Plan Heuristics&lt;/span&gt;

For each component, generate tests from this checklist. Skip categories that don't apply.
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Creation**&lt;/span&gt;: component compiles and renders with required inputs set.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Inputs**&lt;/span&gt;: required inputs render the provided value; optional inputs use defaults; input changes propagate via &lt;span class="sb"&gt;`componentRef.setInput(...)`&lt;/span&gt; + &lt;span class="sb"&gt;`detectChanges()`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Outputs**&lt;/span&gt;: each &lt;span class="sb"&gt;`output()`&lt;/span&gt; emits with the expected payload when its trigger fires.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Signals / computed**&lt;/span&gt;: initial value, updates when dependencies change, edge cases (empty, null, boundary).
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Methods**&lt;/span&gt;: each method produces its documented side effect.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Template**&lt;/span&gt;: &lt;span class="sb"&gt;`@if`&lt;/span&gt; both branches, &lt;span class="sb"&gt;`@for`&lt;/span&gt; empty / one / many, &lt;span class="sb"&gt;`@switch`&lt;/span&gt; each case, event bindings, class/style bindings.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Host**&lt;/span&gt;: host classes, attributes, and listeners behave correctly.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Dependencies**&lt;/span&gt;: injected services replaced with fakes/spies via &lt;span class="sb"&gt;`providers`&lt;/span&gt;.

&lt;span class="gu"&gt;## Spec Template&lt;/span&gt;

// ... a reference spec template with TestBed setup, setInput for required inputs,
//     and sections for inputs / outputs / signals / template ...

&lt;span class="gu"&gt;## Best Practices&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**One behavior per test.**&lt;/span&gt; Each &lt;span class="sb"&gt;`it`&lt;/span&gt; asserts a single observable outcome.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Arrange / Act / Assert**&lt;/span&gt; — keep the three phases visually separated.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Test public behavior, not implementation.**&lt;/span&gt; Drive the component through inputs and DOM events; assert on outputs, rendered DOM, and public signals. Never test &lt;span class="sb"&gt;`private`&lt;/span&gt; members directly.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Use `setInput`, not field assignment.**&lt;/span&gt; Signal inputs must be set via &lt;span class="sb"&gt;`fixture.componentRef.setInput(name, value)`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Call `fixture.detectChanges()`**&lt;/span&gt; after every state change that should affect the view.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Query the DOM with `By.css`**&lt;/span&gt; and assert against &lt;span class="sb"&gt;`textContent`&lt;/span&gt;, attributes, or element presence — not on stringified HTML.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Stub dependencies.**&lt;/span&gt; Replace injected services with jasmine spy objects via &lt;span class="sb"&gt;`{ provide: X, useValue: ... }`&lt;/span&gt; in &lt;span class="sb"&gt;`providers`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Async.**&lt;/span&gt; Use &lt;span class="sb"&gt;`fakeAsync`&lt;/span&gt; + &lt;span class="sb"&gt;`tick`&lt;/span&gt; for timers, &lt;span class="sb"&gt;`await fixture.whenStable()`&lt;/span&gt; for promises.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**No snapshot tests**&lt;/span&gt; for templates — write explicit assertions.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Deterministic.**&lt;/span&gt; No reliance on real dates, random values, or network. Inject or freeze those.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Descriptive names.**&lt;/span&gt; &lt;span class="sb"&gt;`it('emits selected with the row id when the row is clicked')`&lt;/span&gt;, not &lt;span class="sb"&gt;`it('works')`&lt;/span&gt;.

&lt;span class="gu"&gt;## Don'ts&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Don't assign signal inputs directly (&lt;span class="sb"&gt;`component.foo = ...`&lt;/span&gt;) — use &lt;span class="sb"&gt;`setInput`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Don't use &lt;span class="sb"&gt;`TestBed.overrideComponent`&lt;/span&gt; just to swap a template.
&lt;span class="p"&gt;-&lt;/span&gt; Don't test &lt;span class="sb"&gt;`private`&lt;/span&gt; or &lt;span class="sb"&gt;`protected`&lt;/span&gt; members by casting to &lt;span class="sb"&gt;`any`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Don't assert on CSS selectors that include Angular-generated attributes (&lt;span class="sb"&gt;`_ngcontent-...`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Don't write a single giant &lt;span class="sb"&gt;`it`&lt;/span&gt; that exercises every behavior.
&lt;span class="p"&gt;-&lt;/span&gt; Don't re-test framework behavior (that &lt;span class="sb"&gt;`@Input`&lt;/span&gt; works, that &lt;span class="sb"&gt;`output()`&lt;/span&gt; emits at all) — test your component's use of it.
&lt;span class="p"&gt;-&lt;/span&gt; Don't modify the component under test to make it easier to test without asking first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full version, including the complete spec template and a worked example, is in the &lt;a href="https://github.com/brianmtreese/create-angular-skills-cursor" rel="noopener noreferrer"&gt;project repo&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Notice the same pattern as before: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assumptions&lt;/li&gt;
&lt;li&gt;A workflow&lt;/li&gt;
&lt;li&gt;A template&lt;/li&gt;
&lt;li&gt;Best practices&lt;/li&gt;
&lt;li&gt;And a list of don'ts. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The don'ts are what stop the AI from falling back to legacy patterns like assigning signal inputs with &lt;code&gt;component.foo = ...&lt;/code&gt; or casting to &lt;code&gt;any&lt;/code&gt; to poke at private members.&lt;/p&gt;

&lt;p&gt;With that in place, we run &lt;code&gt;/add-component-unit-tests&lt;/code&gt; against our &lt;code&gt;HelloWorldComponent&lt;/code&gt; and get a full spec file that seeds the required signal input, exercises the output with a payload assertion, and verifies the rendered template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ComponentFixture&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TestBed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core/testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;By&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/platform-browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HelloWorldComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./hello-world.component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HelloWorldComponent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ComponentFixture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HelloWorldComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HelloWorldComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;TestBed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureTestingModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;HelloWorldComponent&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;compileComponents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;fixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TestBed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HelloWorldComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;componentInstance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;componentRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todayDate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Monday, April 20, 2026&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;creates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inputs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders the provided todayDate value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hello-world__date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Monday, April 20, 2026&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reflects updates to todayDate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;componentRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todayDate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tuesday, April 21, 2026&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hello-world__date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tuesday, April 21, 2026&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outputs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emits updateTime exactly once when the button is clicked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emitSpy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emitSpy&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledTimes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emits an ISO-formatted string as the updateTime payload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emitSpy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emitSpy&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;jasmine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringMatching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{4}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;T&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\d{2}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;template&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders the heading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello, world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders the date inside the .hello-world__date element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hello-world__date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders a button with the correct type and label&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;By&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Update time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lot of the skill's rules are baked into this output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The hardcoded date isn't magic, it comes from the &lt;code&gt;setInput&lt;/code&gt; call in &lt;code&gt;beforeEach&lt;/code&gt;. The skill enforces seeding required signal inputs &lt;em&gt;before&lt;/em&gt; the first &lt;code&gt;detectChanges&lt;/code&gt;, so the template has the data it needs to render.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;inputs&lt;/strong&gt; block covers both the initial render and how the component responds to &lt;code&gt;setInput&lt;/code&gt; updates, not just "does it work once?" but "does it react to change?"&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;outputs&lt;/strong&gt; block goes beyond "was it called?" and also asserts on the &lt;strong&gt;payload shape&lt;/strong&gt; with &lt;code&gt;jasmine.stringMatching(...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;template&lt;/strong&gt; block queries the rendered DOM for each meaningful element, heading, date container, button, rather than stringifying HTML or relying on snapshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that comes out of the box because we told the AI once, in the skill, how we expect our team's specs to look.&lt;/p&gt;

&lt;p&gt;And then, we can run &lt;code&gt;npm test&lt;/code&gt; to see the tests pass:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-tests-passing.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-23%2Fcursor-skills-tests-passing.jpg" alt="The tests passing in the terminal after running npm test." width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills vs. Rules: When to Use Which
&lt;/h2&gt;

&lt;p&gt;If you're already using Cursor Rules (&lt;code&gt;.cursor/rules/&lt;/code&gt;) or &lt;code&gt;AGENTS.md&lt;/code&gt;, you might be wondering where Skills fit in. &lt;/p&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rules&lt;/strong&gt; are always-on guidance. They apply automatically to every request that matches their scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt; are on-demand workflows. You invoke them explicitly when you want that specific behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rules are great for baseline conventions ("we use signals in this repo"). &lt;/p&gt;

&lt;p&gt;Skills are great for &lt;strong&gt;repeatable, parameterized tasks&lt;/strong&gt; ("scaffold a new component", "generate tests for this file") where you want a guided, interactive workflow instead of passive nudging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts: Automating Angular Workflows with Cursor Skills
&lt;/h2&gt;

&lt;p&gt;Here’s the real takeaway: with skills, Cursor isn’t just generating code, it’s letting you encode how your team builds software.&lt;/p&gt;

&lt;p&gt;And this is just the beginning.&lt;/p&gt;

&lt;p&gt;Because next, we could apply something like this to migrate an existing reactive form over to signal forms, which gets way more interesting!&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Ahead of Angular's Next Shift
&lt;/h2&gt;

&lt;p&gt;And speaking of signal forms, they’re still pretty 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;/p&gt;

&lt;p&gt;You can access it either directly or through YouTube membership, depending on what works best for you:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Buy the course&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://www.youtube.com/channel/UCdPhLDznZzUeEtshDUe0R_A/join" rel="noopener noreferrer"&gt;Get it with YouTube membership&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/create-angular-skills-cursor" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cursor.com/docs/skills" rel="noopener noreferrer"&gt;Cursor Skills Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;My course "Angular Signal Forms: Build Modern Forms with Signals"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>angular</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Better Numeric Inputs in Angular (Signal Forms + Angular 22)</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 17 Apr 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/better-numeric-inputs-in-angular-signal-forms-angular-22-2pal</link>
      <guid>https://dev.to/brianmtreese/better-numeric-inputs-in-angular-signal-forms-angular-22-2pal</guid>
      <description>&lt;p&gt;&lt;span&gt;S&lt;/span&gt;ignal Forms just fixed a subtle, but important, issue you’ve likely shipped without realizing. If you’re using &lt;code&gt;&amp;lt;input type="number"&amp;gt;&lt;/code&gt;, it's likely that you're introducing UX issues that only show up during real interaction. In this example, I'll show you a better approach that will be available in Angular v22.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/oHC9CiZTnFk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Number Inputs Break UX in Angular Forms
&lt;/h2&gt;

&lt;p&gt;Let's start with a typical setup.&lt;/p&gt;

&lt;p&gt;We have a typed form model where age is a number and can be null:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SignupFormData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we have a signal-backed model to store the form data where the age field is initialized to null:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SignupFormData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, we have the form configuration created with the &lt;a href="https://angular.dev/api/forms/signals/form?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;form()&lt;/a&gt; function from the Signal Forms API and our model signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nx"&gt;signupForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Username is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Age is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This form is already setup with basic &lt;a href="https://angular.dev/api/forms/signals/required?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;required()&lt;/a&gt; validators for the username, email, and age fields.&lt;/p&gt;

&lt;p&gt;So this is what we're starting with. &lt;/p&gt;

&lt;p&gt;Now let's finish adding the logic for the age field.&lt;/p&gt;

&lt;p&gt;First, we want to prevent folks from joining if they're under 18, so let's add a &lt;a href="https://angular.dev/api/forms/signals/min?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;min()&lt;/a&gt; validator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You must be at least 18&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we want to ensure the age entered is valid.&lt;/p&gt;

&lt;p&gt;If it's greater than 120, it's probably not valid, so let's add a &lt;a href="https://angular.dev/api/forms/signals/max?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;max()&lt;/a&gt; validator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please enter a valid age&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all we need here. &lt;/p&gt;

&lt;p&gt;Now let’s switch over to &lt;a href="https://github.com/brianmtreese/angular-signal-forms-number-inputs/blob/main/src/form/form.component.html" rel="noopener noreferrer"&gt;the template&lt;/a&gt; and add the field itself.&lt;/p&gt;

&lt;p&gt;Since age will always be a number, we should use a number input, right?&lt;/p&gt;

&lt;p&gt;Let's try it!&lt;/p&gt;

&lt;p&gt;We'll add a number type input and bind it to the age field using the &lt;a href="https://angular.dev/api/forms/signals/formField?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;formField&lt;/a&gt; directive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; 
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; 
  &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"signupForm.age"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, this probably looks correct, but it really isn’t.&lt;/p&gt;

&lt;p&gt;This is one of those cases where the default looks right but causes subtle issues in real use.&lt;/p&gt;

&lt;p&gt;For one, the browser will automatically add a spinner control to the input:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-spinner.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-spinner.jpg" alt="The number input with a spinner control" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are rarely useful except in cases where you actually have an incremental number.&lt;/p&gt;

&lt;p&gt;Which maybe you could argue we have here, but who wants to enter their age this way?&lt;/p&gt;

&lt;p&gt;Also, if you use your mousewheel over the input, it will change the value too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-mousewheel.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-mousewheel.gif" alt="The number input with a mousewheel" width="1124" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our case this isn't too bad but think of something like a postal code or credit card CVV number.&lt;/p&gt;

&lt;p&gt;It just wouldn't make sense.&lt;/p&gt;

&lt;p&gt;And this isn't just something I'm making up, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number#using_number_inputs" rel="noopener noreferrer"&gt;MDN explicitly recommends avoiding number inputs in many cases&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, let's switch over to the recommended approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Switch to a Text Input
&lt;/h2&gt;

&lt;p&gt;For this, all we need to do is replace this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; 
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; 
  &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"signupForm.age"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; 
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; 
  &lt;span class="na"&gt;inputmode=&lt;/span&gt;&lt;span class="s"&gt;"numeric"&lt;/span&gt; 
  &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"signupForm.age"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes browser-controlled behavior while still triggering the numeric keyboard on mobile thanks to the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode" rel="noopener noreferrer"&gt;inputmode attribute&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At this point (pre-Angular 22), this breaks typing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-typing.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-typing.jpg" alt="The number input with a typing issue" width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Angular 22 Fix: Number ↔ Text Binding
&lt;/h2&gt;

&lt;p&gt;Previously, with Signal Forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text input → &lt;code&gt;string&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Model expects → &lt;code&gt;number | null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Result → type mismatch
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Angular 22 fixes this.&lt;/p&gt;

&lt;p&gt;After upgrading, Signal Forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept text inputs for numeric fields
&lt;/li&gt;
&lt;li&gt;Convert values to &lt;code&gt;number&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Map empty input to &lt;code&gt;null&lt;/code&gt; (not &lt;code&gt;''&lt;/code&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the key improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Keep Validation in the Schema
&lt;/h2&gt;

&lt;p&gt;If we want to strictly adhere to the MDN guidance we would add attributes like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; 
  &lt;span class="na"&gt;pattern=&lt;/span&gt;&lt;span class="s"&gt;"[0-9]*"&lt;/span&gt; 
  &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; 
  &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"120"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But these aren’t needed here.&lt;/p&gt;

&lt;p&gt;With Signal Forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validation belongs in the schema
&lt;/li&gt;
&lt;li&gt;Template stays declarative
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we'll keep the validation where we have it, and we'll remove these attributes.&lt;/p&gt;




&lt;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" " width="800" height="416"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: Restrict Input via Keyboard Handling
&lt;/h2&gt;

&lt;p&gt;According to MDN, browsers are inconsistent at enforcing numeric input, even with the correct &lt;code&gt;inputmode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So we need to enforce it ourselves.&lt;/p&gt;

&lt;p&gt;To do this, we'll add a (keydown) event handler to the input.&lt;/p&gt;

&lt;p&gt;We'll call it onAgeKeydown and it will take a KeyboardEvent parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
  &lt;span class="na"&gt;inputmode=&lt;/span&gt;&lt;span class="s"&gt;"numeric"&lt;/span&gt;
  &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"signupForm.age"&lt;/span&gt;
  &lt;span class="na"&gt;(keydown)=&lt;/span&gt;&lt;span class="s"&gt;"onAgeKeydown($event)"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we'll switch over to &lt;a href="https://github.com/brianmtreese/angular-signal-forms-number-inputs/blob/main/src/form/form.component.ts" rel="noopener noreferrer"&gt;the component TypeScript&lt;/a&gt; and add this new method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;onAgeKeydown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowedKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Backspace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Escape&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ArrowLeft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ArrowRight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowedKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only digits are entered
&lt;/li&gt;
&lt;li&gt;Navigation keys still work
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Final Result
&lt;/h2&gt;

&lt;p&gt;So now, we no longer have the spinner UI or scroll-wheel side effects:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-final-result.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-final-result.jpg" alt="The number input with the final result" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We still can't add any non-numeric characters.&lt;/p&gt;

&lt;p&gt;If we add invalid age values, we'll get the validation errors we added earlier:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-validation-errors.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-validation-errors.jpg" alt="The number input with validation errors" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, when we clear the field, we'll get the correct null value:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-clear-field.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-16%2Fnumber-input-clear-field.jpg" alt="The number input with the clear field" width="800" height="542"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Previously, a text field would give you an empty string, but Angular now handles the conversion to &lt;code&gt;null&lt;/code&gt; for us perfectly!&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean, Typed Numeric Input in Angular 22
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;input type="number"&amp;gt;&lt;/code&gt; looks correct but introduces avoidable UX issues.&lt;/p&gt;

&lt;p&gt;Angular 22 removes the main blocker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can bind numeric models to text inputs cleanly
&lt;/li&gt;
&lt;li&gt;You retain strict typing and schema validation
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For real applications, this is the better default (in many cases):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;type="text"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inputmode="numeric"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Schema validation
&lt;/li&gt;
&lt;li&gt;Explicit keyboard handling
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s a small change that eliminates a class of subtle UX bugs that often slip through reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking This Further with Signal Forms
&lt;/h2&gt;

&lt;p&gt;This example is just one piece of what Signal Forms are starting to simplify.&lt;/p&gt;

&lt;p&gt;If you want to go deeper, I put together a full course that walks through building real-world forms step by step.&lt;/p&gt;

&lt;p&gt;You can access it either directly or through YouTube membership, depending on what works best for you:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Buy the course&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://www.youtube.com/channel/UCdPhLDznZzUeEtshDUe0R_A/join" rel="noopener noreferrer"&gt;Get it with YouTube membership&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-signal-forms-number-inputs" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/41b1410cb8a333a2ce6569483cd10866effc154d#diff-274ce409fbc7e4a00a7b038f6468db85cd1fa6590c60d39a8e138e9a98410484" rel="noopener noreferrer"&gt;The commit that makes this all possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number#using_number_inputs" rel="noopener noreferrer"&gt;MDN guidance on number inputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;My course "Angular Signal Forms: Build Modern Forms with Signals"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>How to Get Specific Validation Errors with Angular Signal Forms</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 10 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/how-to-get-specific-validation-errors-with-angular-signal-forms-27k2</link>
      <guid>https://dev.to/brianmtreese/how-to-get-specific-validation-errors-with-angular-signal-forms-27k2</guid>
      <description>&lt;p&gt;&lt;span&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;p&gt;  &lt;iframe src="https://www.youtube.com/embed/gfYXk9p_PG4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Fpassword-checklist-form.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Fpassword-checklist-form.jpg" alt="A sign-up form with a password checklist showing requirements for uppercase, number, special character, and length" width="800" height="635"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Fpassword-checklist-form-success.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Fpassword-checklist-form-success.gif" alt="A sign-up form with a password checklist showing requirements for uppercase, number, special character, and length" width="760" height="518"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  Why Error Indexes Break in Angular Signal Forms
&lt;/h2&gt;

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

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

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

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

  &lt;span class="c1"&gt;// Password must include at least one special character&lt;/span&gt;
  &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;missingSpecialChar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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="https://angular.dev/api/forms/signals/minLength?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;minLength validator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The second is a &lt;a href="https://angular.dev/api/forms/signals/validate?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;[class.valid]=&lt;/span&gt;&lt;span class="s"&gt;"!form.password().errors()[2]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    One uppercase letter
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Ferror-index-bug.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Ferror-index-bug.jpg" alt="The minLength error is not showing up yet because the field is empty" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&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&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Ferror-index-bug-2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Ferror-index-bug-2.jpg" alt="Typing in the password field causes the error array order to change, breaking the UI logic" width="800" height="700"&gt;&lt;/a&gt;&lt;/p&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&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="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find" rel="noopener noreferrer"&gt;find()&lt;/a&gt; method instead where the kind equals &lt;code&gt;missingUppercase&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;[class.valid]=&lt;/span&gt;&lt;span class="s"&gt;"!form.password().errors().find(e =&amp;gt; e.kind === 'missingUppercase')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    One uppercase letter
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Ffind-success.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Ffind-success.jpg" alt="Typing in the password field successfully checks off each requirement one by one" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&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&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&gt;hasError()&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;[class.valid]=&lt;/span&gt;&lt;span class="s"&gt;"!signUpForm.controls.password.hasError('missingUppercase')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    One uppercase letter
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ol&lt;/span&gt; &lt;span class="na"&gt;[class.dirty]=&lt;/span&gt;&lt;span class="s"&gt;"form.password().dirty()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;[class.valid]=&lt;/span&gt;&lt;span class="s"&gt;"!form.password().getError('missingUppercase')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    One uppercase letter
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;[class.valid]=&lt;/span&gt;&lt;span class="s"&gt;"!form.password().getError('missingNumber')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    One number
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;[class.valid]=&lt;/span&gt;&lt;span class="s"&gt;"!form.password().getError('missingSpecialChar')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    One special character
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;[class.valid]=&lt;/span&gt;&lt;span class="s"&gt;"!form.password().getError('minLength') &amp;amp;&amp;amp; form.password().value()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    At least 8 characters
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;getError()&lt;/code&gt; will only return the first one. &lt;/p&gt;

&lt;p&gt;Also, for the &lt;code&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;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" " width="800" height="416"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Final Result
&lt;/h2&gt;

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

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-09%2Fgeterror-success.gif" alt="Typing in the password field successfully checks off each requirement one by one" width="720" height="459"&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&gt;
  
  
  Why getError() Makes Signal Forms Easier to Use
&lt;/h2&gt;

&lt;p&gt;So &lt;code&gt;getError()&lt;/code&gt; brings Signal Forms much closer to the ergonomics we had with &lt;code&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&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="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms: Build Modern Forms with Signals&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/Getting-Specific-Validation-Errors-with-Signal-Forms" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/709f5a390ca0de04f8066012a5cb36999f2fd4a6" rel="noopener noreferrer"&gt;The commit that makes this all possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;My course "Angular Signal Forms: Build Modern Forms with Signals"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Angular 22's New Built-in Debounce for Async Validation Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 03 Apr 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-22s-new-built-in-debounce-for-async-validation-explained-j9k</link>
      <guid>https://dev.to/brianmtreese/angular-22s-new-built-in-debounce-for-async-validation-explained-j9k</guid>
      <description>&lt;p&gt;&lt;span&gt;I&lt;/span&gt;f you're using &lt;a href="https://angular.dev/essentials/signal-forms?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Signal Forms&lt;/a&gt; with async validation, you've probably run into a frustrating issue. You either debounce every validator with the &lt;a href="https://angular.dev/api/forms/signals/debounce?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&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="https://github.com/angular/angular/commit/24e52d450d201e3da90bb64f84358f9eccd7877d" rel="noopener noreferrer"&gt;built-in debounce&lt;/a&gt; works and why it makes Signal Forms even better.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4ynDt0-Cj7A"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Ftyping-slowly.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Ftyping-slowly.gif" alt="Typing slowly in the username field without triggering validation" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Fpending-message.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Fpending-message.jpg" alt="Pending message showing the username is being validated" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Fvalidation-error.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Fvalidation-error.jpg" alt="Validation error showing the username already exists" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Femail-validation-error.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Femail-validation-error.gif" alt="An email field using validateHttp() and debounce()" width="760" height="313"&gt;&lt;/a&gt;&lt;/p&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&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&gt;model&lt;/code&gt; signal holding the state for our sign up form, and a &lt;code&gt;form&lt;/code&gt; declaration.&lt;br&gt;
&lt;/p&gt;

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

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

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

  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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="https://angular.dev/api/forms/signals/required?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;required()&lt;/a&gt; or &lt;a href="https://angular.dev/api/forms/signals/minLength?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&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="https://angular.dev/api/forms/signals/validateHttp?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;validateHttp()&lt;/a&gt; on the email field:&lt;br&gt;
&lt;/p&gt;

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

  &lt;span class="nf"&gt;validateHttp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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&gt;debounce&lt;/code&gt; option directly into the validation functions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Angular 22 Fix: Built-in Debounce for Async Validators
&lt;/h3&gt;

&lt;p&gt;So, we can delete the old &lt;code&gt;debounce&lt;/code&gt; function call from and instead, inside &lt;code&gt;validateAsync()&lt;/code&gt; and &lt;code&gt;validateHttp()&lt;/code&gt;, we can just add the &lt;code&gt;debounce&lt;/code&gt; property directly:&lt;br&gt;
&lt;/p&gt;

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

  &lt;span class="nf"&gt;validateHttp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// debounce(s.email, 2000);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" " width="800" height="416"&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&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;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F04-02%2Ffinal-result.gif" alt="Typing in the username field with debounced async validation with the new debounce option" width="800" height="249"&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&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&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="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms: Build Modern Forms with Signals&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-signal-forms-debounce-async-validation" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/24e52d450d201e3da90bb64f84358f9eccd7877d" rel="noopener noreferrer"&gt;The commit that makes this all possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;My course "Angular Signal Forms: Build Modern Forms with Signals"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Angular 22: Mix Signal Forms and Reactive Forms Seamlessly</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 27 Mar 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-22-mix-signal-forms-and-reactive-forms-seamlessly-3p6k</link>
      <guid>https://dev.to/brianmtreese/angular-22-mix-signal-forms-and-reactive-forms-seamlessly-3p6k</guid>
      <description>&lt;p&gt;&lt;span&gt;W&lt;/span&gt;hat if you could start using Signal Forms today without touching your existing &lt;a href="https://angular.dev/guide/forms/reactive-forms" rel="noopener noreferrer"&gt;Reactive&lt;/a&gt; or &lt;a href="https://angular.dev/guide/forms/template-driven-forms" rel="noopener noreferrer"&gt;Template-driven&lt;/a&gt; forms at all? In Angular 22, you'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="https://angular.dev/api/forms/ControlValueAccessor" rel="noopener noreferrer"&gt;ControlValueAccessor&lt;/a&gt; to &lt;a href="https://angular.dev/api/forms/signals/FormValueControl" rel="noopener noreferrer"&gt;FormValueControl&lt;/a&gt; while keeping the parent form completely intact.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/RQUFjZdFqGE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fcart-form-demo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fcart-form-demo.jpg" alt="A cart form with a quantity stepper, coupon code, email, and gift wrap checkbox" width="998" height="1058"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support/tree/main/src/app/quantity-stepper" rel="noopener noreferrer"&gt;quantity control&lt;/a&gt; is actually a custom form control built using &lt;code&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fcustom-control-updating-value.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fcustom-control-updating-value.jpg" alt="Clicking the plus and minus buttons updates the quantity value" width="1080" height="544"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fcart-form-validation.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fcart-form-validation.jpg" alt="Changing the quantity below 1 triggers a validation error message" width="1050" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything works, but the underlying &lt;code&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&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="https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support/tree/main/src/app/cart" rel="noopener noreferrer"&gt;the parent form&lt;/a&gt; component. &lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support/blob/main/src/app/cart/cart.component.html" rel="noopener noreferrer"&gt;the template&lt;/a&gt;, we have a &lt;code&gt;formGroup&lt;/code&gt; directive that wraps all of our form controls:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Within this form group, our custom &lt;code&gt;app-quantity-stepper&lt;/code&gt; component is wired up using the standard &lt;code&gt;formControlName&lt;/code&gt; directive as well:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;All the other fieds use the same &lt;code&gt;formControlName&lt;/code&gt; directive too.&lt;/p&gt;

&lt;p&gt;Now let's switch and look at &lt;a href="https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support/blob/main/src/app/cart/cart.component.ts" rel="noopener noreferrer"&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="https://angular.dev/api/forms/FormControl" rel="noopener noreferrer"&gt;FormControls&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;FormControl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Then we have the &lt;a href="https://angular.dev/api/forms/FormGroup" rel="noopener noreferrer"&gt;FormGroup&lt;/a&gt; itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FormGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ReactiveFormsModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Validators&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;FormGroup&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CartForm&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;nonNullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;couponCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nonNullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;nonNullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;giftWrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nonNullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;
  
  
  Why ControlValueAccessor Is So Verbose
&lt;/h2&gt;

&lt;p&gt;First, we have this &lt;code&gt;providers&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;forwardRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;NG_VALUE_ACCESSOR&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We have to provide &lt;code&gt;NG_VALUE_ACCESSOR&lt;/code&gt; and use &lt;code&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&gt;ControlValueAccessor&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;ControlValueAccessor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



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

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

&lt;/div&gt;



&lt;p&gt;After this, we implement empty &lt;code&gt;onChange&lt;/code&gt; and &lt;code&gt;onTouched&lt;/code&gt; callbacks, and wire up &lt;code&gt;writeValue&lt;/code&gt;, &lt;code&gt;registerOnChange&lt;/code&gt;, &lt;code&gt;registerOnTouched&lt;/code&gt;, and &lt;code&gt;setDisabledState&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

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

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

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

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

&lt;span class="nf"&gt;setDisabledState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDisabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;ControlValueAccessor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, in our &lt;code&gt;increment&lt;/code&gt; and &lt;code&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&gt;onChange&lt;/code&gt; so the parent form knows about it:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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&gt;providers&lt;/code&gt; array, the private &lt;code&gt;value&lt;/code&gt; signal, and all of those &lt;code&gt;ControlValueAccessor&lt;/code&gt; methods. &lt;/p&gt;

&lt;p&gt;Instead of &lt;code&gt;ControlValueAccessor&lt;/code&gt;, we'll implement the new &lt;code&gt;FormValueControl&lt;/code&gt; interface, and we'll type it to a number.&lt;br&gt;
&lt;/p&gt;

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

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

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

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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="https://angular.dev/api/core/model" rel="noopener noreferrer"&gt;model()&lt;/a&gt; signal for the value of the control. &lt;/p&gt;

&lt;p&gt;We also changed our &lt;code&gt;isDisabled&lt;/code&gt; property to an &lt;a href="https://angular.dev/api/core/input" rel="noopener noreferrer"&gt;input&lt;/a&gt; initialized to false. &lt;/p&gt;

&lt;p&gt;We don’t need to call &lt;code&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;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" "&gt;&lt;br&gt;
&lt;/a&gt;&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&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&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;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fsignal-form-control-working.jpg" alt="The updated form still works perfectly with the new Signal-based child control" width="1006" height="1068"&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;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fsignal-form-control-updating-value.jpg" alt="The value of our Reactive Form is updating correctly in real time, now driven by our Signal-based child control" width="1032" height="530"&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;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-26%2Fsignal-form-control-validation-error.jpg" alt="The error message appearing when the quantity is below 1" width="1010" height="422"&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&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&gt;FormGroup&lt;/code&gt;, &lt;code&gt;FormControl&lt;/code&gt;, and the &lt;code&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&gt;
  
  
  Get Ahead of Angular's Next Shift
&lt;/h2&gt;

&lt;p&gt;Most Angular apps today still rely on &lt;code&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="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms: Build Modern Forms with Signals&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/template-and-reactive-forms-form-value-control-support" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/c4ce3f345fdb14595f0991dff488c4043a0fc71c" rel="noopener noreferrer"&gt;The commit that makes this all possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/forms/signals/custom-controls" rel="noopener noreferrer"&gt;Angular Signal Forms Custom Controls Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;My course "Angular Signal Forms: Build Modern Forms with Signals"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Angular’s New debounced() Signal Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 20 Mar 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angulars-new-debounced-signal-explained-2gg6</link>
      <guid>https://dev.to/brianmtreese/angulars-new-debounced-signal-explained-2gg6</guid>
      <description>&lt;p&gt;&lt;span&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="https://rxjs.dev/api/operators/debounceTime" rel="noopener noreferrer"&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="https://github.com/angular/angular/commit/b918beda323eefef17bf1de03fde3d402a3d4af0" rel="noopener noreferrer"&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;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/jKffTaEL1JI"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-19%2Fsimple-product-search-app.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-19%2Fsimple-product-search-app.jpg" alt="A simple product search app" width="1382" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-19%2Fnetwork-spam.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-19%2Fnetwork-spam.gif" alt="A GIF showing the browser's network tab firing a new API request on every keystroke in the search input" width="1908" height="944"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  The Old Way: Debouncing with RxJS &lt;code&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&gt;query&lt;/code&gt; signal that holds the search term, which is converted to an observable using &lt;code&gt;toObservable&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;products&lt;/code&gt; are then loaded inside a &lt;code&gt;toSignal&lt;/code&gt; block that pipes the query observable through several RxJS operators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;$query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The traditional fix is to add the &lt;code&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;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-19%2Fnetwork-spam-fixed-with-rxjs-debounce-time.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-19%2Fnetwork-spam-fixed-with-rxjs-debounce-time.gif" alt="A GIF showing the browser's network tab firing a new API request after the user stops typing for 1 second" width="1918" height="904"&gt;&lt;/a&gt;&lt;/p&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;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" "&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The New Way: &lt;code&gt;debounced()&lt;/code&gt; and &lt;code&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&gt;debounced&lt;/code&gt;, and it can work together with &lt;code&gt;resource&lt;/code&gt; to solve this exact problem elegantly.&lt;/p&gt;
&lt;h3&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&gt;query&lt;/code&gt; signal. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;debounced()&lt;/code&gt; function from &lt;code&gt;@angular/core&lt;/code&gt; makes this trivial.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;debounced&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That's it. &lt;code&gt;debouncedQuery&lt;/code&gt; is now a read-only signal that will only update its value when the &lt;code&gt;query&lt;/code&gt; signal has been stable for 1000 milliseconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Refactor to Use &lt;code&gt;resource()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Next, we'll completely replace our &lt;code&gt;toSignal&lt;/code&gt; implementation with the new &lt;code&gt;resource()&lt;/code&gt; primitive. &lt;/p&gt;

&lt;p&gt;&lt;code&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&gt;products&lt;/code&gt; signal and its &lt;code&gt;toSignal&lt;/code&gt; block and replace it with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debouncedQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nf"&gt;firstValueFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;params&lt;/code&gt;&lt;/strong&gt;: A function that returns the current search query from the debounced signal (&lt;code&gt;this.debouncedQuery.value()&lt;/code&gt;), or &lt;code&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&gt;loader&lt;/code&gt;&lt;/strong&gt;: A function that receives the resolved &lt;code&gt;params&lt;/code&gt; and fetches data using Angular's &lt;code&gt;HttpClient&lt;/code&gt;. Because &lt;code&gt;HttpClient&lt;/code&gt; returns an Observable, &lt;code&gt;firstValueFrom()&lt;/code&gt; is used to convert it to a Promise. The result is then unwrapped to return just the &lt;code&gt;products&lt;/code&gt; array.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;resource&lt;/code&gt; primitive automatically manages the loading, error, and data states for us based on the &lt;code&gt;params&lt;/code&gt; signal and the &lt;code&gt;loader&lt;/code&gt; function's execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating the Template for the &lt;code&gt;resource&lt;/code&gt; API
&lt;/h2&gt;

&lt;p&gt;The new &lt;code&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&gt;status&lt;/code&gt; property, we use methods like &lt;code&gt;isLoading()&lt;/code&gt; and &lt;code&gt;value()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our old &lt;code&gt;@switch&lt;/code&gt; block gets replaced with a set of &lt;code&gt;@if&lt;/code&gt; conditions:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;We first check if the base &lt;code&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&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&gt;products.value()&lt;/code&gt; and render the results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-19%2Fnetwork-spam-fixed-with-debounced-signal.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-19%2Fnetwork-spam-fixed-with-debounced-signal.gif" alt="A GIF showing the browser's network tab firing a new API request after the user stops typing for 1 second" width="1918" height="920"&gt;&lt;/a&gt;&lt;/p&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&gt;toObservable&lt;/code&gt;, no &lt;code&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&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="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-v22-debounced-signals-example" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/signals/resource" rel="noopener noreferrer"&gt;Angular Resource API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rxjs.dev/api/operators/debounceTime" rel="noopener noreferrer"&gt;RxJS debounceTime Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;My course "Angular Signal Forms: Build Modern Forms with Signals"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>rxjs</category>
    </item>
    <item>
      <title>Angular Signal Forms: Is FormValueControl Better for Large Forms?</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 13 Mar 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-signal-forms-is-formvaluecontrol-better-for-large-forms-3i4h</link>
      <guid>https://dev.to/brianmtreese/angular-signal-forms-is-formvaluecontrol-better-for-large-forms-3i4h</guid>
      <description>&lt;p&gt;&lt;span&gt;I&lt;/span&gt;n a &lt;a href="https://briantree.se/angular-signal-forms-structuring-large-forms/" rel="noopener noreferrer"&gt;recent guide&lt;/a&gt; I showed a pattern for building large &lt;a href="https://angular.dev/essentials/signal-forms" rel="noopener noreferrer"&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="https://angular.dev/api/forms/signals/FormValueControl" rel="noopener noreferrer"&gt;FormValueControl&lt;/a&gt; instead?&lt;/em&gt; It sounded like a great idea, so I tried it. In this post you'll see how it works and why I'm not completely convinced it's actually the better approach for this scenario.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/FN0PcwGa7ts"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  The Original Approach
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://briantree.se/angular-signal-forms-structuring-large-forms/" rel="noopener noreferrer"&gt;Structuring Large Forms&lt;/a&gt; - The field tree approach this post compares against&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fprofile-form-original.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fprofile-form-original.jpg" alt="The profile form with the account information, shipping address, and preferences sections" width="1524" height="1406"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fdebug-panel-original.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fdebug-panel-original.jpg" alt="The debug panel showing the real-time value and status of the form" width="1050" height="1056"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Ffirst-name-input-original.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Ffirst-name-input-original.jpg" alt="The first name input field with the value updating in real time" width="1044" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Building Reusable Angular Signal Forms with Field Tree
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/blob/master/src/app/sign-up/profile-form/profile-form.component.html" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Each component receives a slice of the form's field tree through an input called &lt;code&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="https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/blob/master/src/app/sign-up/profile-form/profile-form.component.ts" rel="noopener noreferrer"&gt;the component TypeScript&lt;/a&gt;, we first define the interface for our profile form:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Preferences&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

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

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

    &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

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

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

    &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;
  
  
  Inside a Reusable Angular Form Section Component
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-account-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountFormComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FieldTree&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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="https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/blob/master/src/app/account/account-form/account-form.component.html" rel="noopener noreferrer"&gt;the template&lt;/a&gt;, each input is bound using the &lt;a href="https://angular.dev/api/forms/signals/FormField" rel="noopener noreferrer"&gt;FormField&lt;/a&gt; directive accessing the appropriate field from the input:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Validation errors are shown by passing the control state to a custom &lt;a href="https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example/tree/master/src/app/shared/validation-errors" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SchemaPathTree&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms/signals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

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

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildAccountSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SchemaPathTree&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;First name is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Last name is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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&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&gt;Account&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FormValueControl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms/signals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;When you implement this interface, Angular expects the component to expose a &lt;code&gt;value&lt;/code&gt; &lt;a href="https://angular.dev/api/core/model" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;First name is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Last name is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;form&lt;/code&gt; property instead of the input signal:&lt;/p&gt;

&lt;h4&gt;
  
  
  Before:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"form().firstName"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  After:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"form.firstName"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;FormValueControl&lt;/code&gt;, switches to the &lt;code&gt;value&lt;/code&gt; model input, and creates a local form with its validation:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;/div&gt;



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

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

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

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;br&gt;
&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" "&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&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&gt;
  
  
  Before:
&lt;/h4&gt;


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

&lt;/div&gt;

&lt;h4&gt;
  
  
  After:
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then we need to import the new &lt;code&gt;FormField&lt;/code&gt; directive so that we can use it in the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;FormField&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms/signals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Then we can update the template to use the &lt;code&gt;FormField&lt;/code&gt; directive instead of the old input:&lt;/p&gt;

&lt;h4&gt;
  
  
  Before:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;[formRoot]=&lt;/span&gt;&lt;span class="s"&gt;"form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;app-account-form&lt;/span&gt; &lt;span class="na"&gt;[form]=&lt;/span&gt;&lt;span class="s"&gt;"form().account"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;app-address-form&lt;/span&gt; &lt;span class="na"&gt;[form]=&lt;/span&gt;&lt;span class="s"&gt;"form().shippingAddress"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;app-preferences-form&lt;/span&gt; &lt;span class="na"&gt;[form]=&lt;/span&gt;&lt;span class="s"&gt;"form().preferences"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  After:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;[formRoot]=&lt;/span&gt;&lt;span class="s"&gt;"form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;app-account-form&lt;/span&gt; &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"form.account"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;app-address-form&lt;/span&gt; &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"form.shippingAddress"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;app-preferences-form&lt;/span&gt; &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"form.preferences"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fprofile-form-formvaluecontrol.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fprofile-form-formvaluecontrol.jpg" alt="The profile form with the account information, shipping address, and preferences sections" width="1524" height="1406"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Ffirst-name-input-formvaluecontrol.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Ffirst-name-input-formvaluecontrol.jpg" alt="The first name input field with the value updating in real time" width="1044" height="544"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Flast-name-validation-error-formvaluecontrol.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Flast-name-validation-error-formvaluecontrol.jpg" alt="The last name input field with the validation error" width="988" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fdebug-panel-formvaluecontrol.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fdebug-panel-formvaluecontrol.jpg" alt="The debug panel showing the form object with the account information, shipping address, and preferences sections" width="1102" height="632"&gt;&lt;/a&gt;&lt;/p&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildAccountSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SchemaPathTree&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Account&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildAddressSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SchemaPathTree&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Address&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;street&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;city&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d{5}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fprofile-form-formvaluecontrol-invalid.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fprofile-form-formvaluecontrol-invalid.jpg" alt="The profile form with the account information, shipping address, and preferences sections" width="914" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Flast-name-validation-error-formvaluecontrol.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Flast-name-validation-error-formvaluecontrol.jpg" alt="The last name input field with the validation error" width="988" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fprofile-form-formvaluecontrol-valid.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-12%2Fprofile-form-formvaluecontrol-valid.jpg" alt="The profile form with the account information, shipping address, and preferences sections" width="1258" height="1198"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h2&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&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&gt;FormValueControl&lt;/code&gt;, I'd genuinely love to hear it.&lt;/p&gt;

&lt;h2&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="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/signal-forms-composition-formvaluecontrol-example" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://briantree.se/angular-signal-forms-structuring-large-forms/" rel="noopener noreferrer"&gt;How to Structure Large Forms Without Losing Your Mind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=D25F85A7AC786D432252" rel="noopener noreferrer"&gt;My course "Angular Signal Forms: Build Modern Forms with Signals"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Angular 21 Signal Forms: ignoreValidators Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 06 Mar 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-21-signal-forms-ignorevalidators-explained-3gpb</link>
      <guid>https://dev.to/brianmtreese/angular-21-signal-forms-ignorevalidators-explained-3gpb</guid>
      <description>&lt;p&gt;&lt;span&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="https://angular.dev/essentials/signal-forms" rel="noopener noreferrer"&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'll walk through all three modes so you can choose the right strategy for your real-world Angular applications.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/Fd_8YGeFMTk"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-before.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-before.jpg" alt="The signup form with username field" width="1244" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-minlength-error.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-minlength-error.jpg" alt="The signup form with username field and a minlength validation error" width="1402" height="752"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-checking-availability.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-checking-availability.jpg" alt="The signup form with username field and a checking availability message" width="1401" height="750"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-log.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-log.jpg" alt="The signup form with username field and a submission log" width="2276" height="760"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-error.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-error.jpg" alt="The signup form with username field and a submission error" width="1282" height="756"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  How the Signal Form Is Configured (Template + Submission Logic)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/brianmtreese/angular-signal-forms-submit-options/blob/master/src/form/form.component.html" rel="noopener noreferrer"&gt;The template&lt;/a&gt; uses the new &lt;a href="https://angular.dev/api/forms/signals/FormRoot" rel="noopener noreferrer"&gt;FormRoot&lt;/a&gt; directive on the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element to connect the native form to the Signal Form model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;[formRoot]=&lt;/span&gt;&lt;span class="s"&gt;"form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ...
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text input uses the &lt;a href="https://angular.dev/api/forms/signals/FormField" rel="noopener noreferrer"&gt;FormField&lt;/a&gt; directive to bind to the username field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;
    &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
    &lt;span class="na"&gt;[formField]=&lt;/span&gt;&lt;span class="s"&gt;"form.username"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@if (form.username().pending()) {
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Checking availability...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The submit button is disabled during submission and its label changes to "Creating...":&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  The TypeScript: Model and Validation
&lt;/h3&gt;

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

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

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SignupModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we create the form using the &lt;a href="https://angular.dev/api/forms/signals/form" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Form Value:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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: 'pending'&lt;/code&gt;, you can still submit while async validators are running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;ignoreValidators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-while-async-validator-is-running.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-while-async-validator-is-running.jpg" alt="The signup form with username field and a submission while the async validator is still running" width="2266" height="754"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  ignoreValidators: 'none' - Block Submission Until Validation Completes
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;ignoreValidators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-not-submitting-while-async-validator-is-running.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-not-submitting-while-async-validator-is-running.jpg" alt="The signup form with username field not submitting while the async validator is still running" width="2254" height="762"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-not-submitting-with-invalid-fields.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-not-submitting-with-invalid-fields.jpg" alt="The signup form with username field not submitting with invalid fields" width="2250" height="756"&gt;&lt;/a&gt;&lt;/p&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;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" "&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;ignoreValidators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;onInvalid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;errorSummary&lt;/span&gt;&lt;span class="p"&gt;()?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;fieldTree&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nx"&gt;focusBoundControl&lt;/span&gt;&lt;span class="p"&gt;?.();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-focusing-first-invalid-field.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-focusing-first-invalid-field.gif" alt="The signup form with username field and submission focusing the first invalid field" width="1272" height="778"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  ignoreValidators: 'all' - Submit Even When Invalid or Pending
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;ignoreValidators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-with-empty-required-field.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F03-05%2Fsignup-form-submission-with-empty-required-field.gif" alt="The signup form with username field and a submission with empty required field" width="1908" height="600"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  Choosing the Right ignoreValidators Mode for Real-World Angular Apps
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&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&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&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&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;/div&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="https://www.youtube.com/c/briantreese?sub_confirmation=1" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; for more deep dives into modern Angular features.&lt;/p&gt;

&lt;h2&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="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-signal-forms-submit-options" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/essentials/signal-forms" rel="noopener noreferrer"&gt;Angular Signal Forms Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/forms/validation" rel="noopener noreferrer"&gt;Angular Form Validation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jdoqocy.com/click-101557355-17135603" rel="noopener noreferrer"&gt;Get a Pluralsight FREE TRIAL HERE!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Angular Signal Forms: Number Inputs Finally Fixed in Angular 21.2</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 27 Feb 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-signal-forms-why-number-inputs-were-broken-and-now-arent-1fi7</link>
      <guid>https://dev.to/brianmtreese/angular-signal-forms-why-number-inputs-were-broken-and-now-arent-1fi7</guid>
      <description>&lt;p&gt;&lt;span&gt;N&lt;/span&gt;umber inputs in &lt;a href="https://angular.dev/essentials/signal-forms" rel="noopener noreferrer"&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="https://github.com/angular/angular/releases/tag/v21.2.0" rel="noopener noreferrer"&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;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/3DlNnIsUsMM"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-before.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-before.jpg" alt="The signup form with username and email fields" width="1174" height="1108"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output.jpg" alt="The form model output showing the username and email fields" width="1306" height="388"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  The Obvious Solution (zero)
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://github.com/brianmtreese/angular-signal-forms-null-number-example/blob/master/src/form/form.component.ts" rel="noopener noreferrer"&gt;component TypeScript&lt;/a&gt;, we have a &lt;code&gt;SignupModel&lt;/code&gt; interface that defines the shape of our form data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SignupModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SignupModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;age&lt;/code&gt; is a number, the most obvious default value is zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SignupModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;required&lt;/code&gt; validator for this new &lt;code&gt;age&lt;/code&gt; field:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Adding a Number Input Using Angular Signal Forms
&lt;/h3&gt;

&lt;p&gt;In the template, the &lt;code&gt;age&lt;/code&gt; field follows the same pattern as the other inputs:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;We create an &lt;code&gt;age&lt;/code&gt; template variable to reference the age field signal, and then a &lt;code&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&gt;age&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;The &lt;code&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&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&gt;0&lt;/code&gt; pre-filled:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-zero.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-zero.jpg" alt="The signup form with age field pre-filled with 0" width="1274" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The form model output also shows &lt;code&gt;age: 0&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-zero.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-zero.jpg" alt="The form model output showing the age field with a value of 0" width="1026" height="420"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-required-error.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-required-error.jpg" alt="The signup form with age field blurred showing the required error message" width="858" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the model value becomes &lt;code&gt;null&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-null.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-null.jpg" alt="The form model output showing the age field with a value of null" width="1002" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h2&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&gt;age&lt;/code&gt; can be either a number or a string:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-empty.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-empty.jpg" alt="The signup form with age field empty" width="1198" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-empty-string.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-empty-string.jpg" alt="The form model output showing the age field with a value of empty string" width="1010" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-value.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-value.jpg" alt="The signup form with age field entered with a value" width="1098" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But we've introduced a different problem: from TypeScript's perspective, &lt;code&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&gt;
  
  
  The Hack Developers Used (NaN)
&lt;/h2&gt;

&lt;p&gt;Another approach developers used was &lt;code&gt;NaN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So we revert the interface back to &lt;code&gt;number&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Then we initialize it with &lt;code&gt;NaN&lt;/code&gt; instead of an empty string or zero:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-empty.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-empty.jpg" alt="The signup form with age field empty" width="1198" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the model shows &lt;code&gt;null&lt;/code&gt; this time, which looks correct:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-null.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-null.jpg" alt="The form model output showing the age field with a value of null" width="1002" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But &lt;code&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&gt;number&lt;/code&gt;. &lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;NaN&lt;/code&gt; was a workaround, not a real solution.&lt;/p&gt;




&lt;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" "&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&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&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&gt;null&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Then initialize with &lt;code&gt;null&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SignupModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-empty.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-age-field-empty.jpg" alt="The signup form with age field empty" width="1198" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the model value is still &lt;code&gt;null&lt;/code&gt; to start:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-null.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-26%2Fsignup-form-model-output-age-null.jpg" alt="The form model output showing the age field with a value of null" width="1002" height="406"&gt;&lt;/a&gt;&lt;/p&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&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&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&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="https://www.youtube.com/c/briantreese?sub_confirmation=1" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; for more deep dives into modern Angular features.&lt;/p&gt;

&lt;h2&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="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-signal-forms-null-number-example" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/releases/tag/v21.2.0-rc.0" rel="noopener noreferrer"&gt;v21.2.0-rc.0 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/essentials/signal-forms" rel="noopener noreferrer"&gt;Angular Signal Forms Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/releases" rel="noopener noreferrer"&gt;Angular Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jdoqocy.com/click-101557355-17135603" rel="noopener noreferrer"&gt;Get a Pluralsight FREE TRIAL HERE!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Angular Signal Forms: The New formRoot Directive Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 20 Feb 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-signal-forms-the-new-formroot-directive-explained-509a</link>
      <guid>https://dev.to/brianmtreese/angular-signal-forms-the-new-formroot-directive-explained-509a</guid>
      <description>&lt;p&gt;&lt;span&gt;F&lt;/span&gt;orm submission in &lt;a href="https://angular.dev/essentials/signal-forms" rel="noopener noreferrer"&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't feel fully Angular. Starting in &lt;a href="https://github.com/angular/angular/releases/tag/v21.2.0-next.3" rel="noopener noreferrer"&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;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/ARl78Z0XE7M"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-before-formroot.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-before-formroot.jpg" alt="Signal Forms signup form with username and email fields" width="1096" height="882"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-submit-button-disabled.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-submit-button-disabled.jpg" alt="Signal Forms signup form with submit button disabled due to invalid form state" width="1246" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-submit-button-enabled.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-submit-button-enabled.jpg" alt="Signal Forms signup form with username and email filled in and submit button enabled" width="1284" height="890"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-console-output.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-console-output.jpg" alt="Console showing dirty status, form value, and model signal value after Signal Form submission" width="1304" height="308"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  Manual Form Submission in Signal Forms (The Old Way)
&lt;/h2&gt;

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

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&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&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&gt;formField&lt;/code&gt; directive to connect to the Signal Form:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

&lt;h3&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;br&gt;
&lt;/p&gt;

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

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

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

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;form&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please enter a username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nf"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your username must be at least 3 characters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please enter an email address&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

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

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

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

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

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

&lt;/div&gt;



&lt;p&gt;&lt;code&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&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&gt;formRoot&lt;/code&gt; directive eliminates some of it.&lt;/p&gt;




&lt;p&gt;If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.&lt;/p&gt;

&lt;p&gt;Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.&lt;/p&gt;

&lt;p&gt;👉 Details here: &lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;https://bit.ly/4tfqleD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/4tfqleD" rel="noopener noreferrer"&gt;&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfqdfdnjh1nozt9fe20.png" alt=" "&gt;&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Angular formRoot Directive Explained (Signal Forms)
&lt;/h2&gt;

&lt;p&gt;Starting in Angular 21.2-next.3, the &lt;code&gt;FormRoot&lt;/code&gt; directive is now available from &lt;code&gt;@angular/forms/signals&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Add FormRoot to the Component Imports
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="nx"&gt;FormRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms/signals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Update the Template
&lt;/h3&gt;

&lt;p&gt;Here we remove the &lt;code&gt;(submit)&lt;/code&gt; binding and &lt;code&gt;novalidate&lt;/code&gt; attribute and replace them with the &lt;code&gt;formRoot&lt;/code&gt; directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;[formRoot]=&lt;/span&gt;&lt;span class="s"&gt;"form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ...
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&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&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&gt;form()&lt;/code&gt; function containing a &lt;code&gt;submission&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;We can essentially move everything from the &lt;code&gt;submit()&lt;/code&gt; function in the &lt;code&gt;onSubmit()&lt;/code&gt; method here:&lt;br&gt;
&lt;/p&gt;

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

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

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

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

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

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

&lt;/div&gt;



&lt;p&gt;The old &lt;code&gt;onSubmit()&lt;/code&gt; method can now be removed entirely. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;formRoot&lt;/code&gt; directive connects the DOM &lt;code&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&gt;submission.action&lt;/code&gt; callback. &lt;/p&gt;

&lt;p&gt;This mirrors how &lt;code&gt;formGroup&lt;/code&gt; connects a DOM &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; to a &lt;code&gt;FormGroup&lt;/code&gt; in Reactive Forms.&lt;/p&gt;

&lt;h2&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-formroot-form-after-switching-to-formroot.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-formroot-form-after-switching-to-formroot.jpg" alt="The form after migrating to formRoot" width="1096" height="882"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-formroot-console-output-after-submit.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-formroot-console-output-after-submit.jpg" alt="Console showing the same dirty status, form value, and model signal value after migrating to formRoot and submitting the form" width="1304" height="308"&gt;&lt;/a&gt;&lt;/p&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&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&gt;reset()&lt;/code&gt; inside the submission action:&lt;br&gt;
&lt;/p&gt;

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

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

                &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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&gt;true&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-console-output.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-console-output.jpg" alt="Console showing the dirty status changed from true to false after resetting the form" width="1356" height="614"&gt;&lt;/a&gt;&lt;/p&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&gt;
  
  
  How to Reset Form State AND Model Signal
&lt;/h3&gt;

&lt;p&gt;According to the Signal Forms docs, &lt;code&gt;reset()&lt;/code&gt; does several things.&lt;/p&gt;

&lt;p&gt;First, it says that this resets the &lt;code&gt;touched&lt;/code&gt; and &lt;code&gt;dirty&lt;/code&gt; states of the field and its descendants.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-docs-1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-docs-1.jpg" alt="Signal Forms reset() docs part 1" width="1882" height="176"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-docs-2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-docs-2.jpg" alt="Signal Forms reset() docs part 2" width="1902" height="172"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-docs-3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-docs-3.jpg" alt="Signal Forms reset() docs part 3" width="1796" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;So calling &lt;code&gt;reset()&lt;/code&gt; alone will correctly reset &lt;code&gt;dirty&lt;/code&gt; to &lt;code&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&gt;reset()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Passing a value to &lt;code&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-after-submit.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F02-19%2Fsignal-form-reset-after-submit.jpg" alt="Signal Form fields cleared and console showing reset dirty status, empty form value, and empty model signal after submission" width="1136" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why formRoot Is Probably the New Recommended Pattern
&lt;/h2&gt;

&lt;p&gt;The &lt;code&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&gt;submit()&lt;/code&gt; function directly when you need fine-grained control, but for most forms, &lt;code&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&gt;formGroup&lt;/code&gt; declaratively connect the DOM to the form model. &lt;/p&gt;

&lt;p&gt;&lt;code&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="https://www.youtube.com/c/briantreese?sub_confirmation=1" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt; for more deep dives into modern Angular features.&lt;/p&gt;

&lt;h2&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="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/fZZ1UVkyB4I"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-signal-forms-form-root-example" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/releases/tag/v21.2.0-next.3" rel="noopener noreferrer"&gt;v21.2.0-next.3 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/essentials/signal-forms" rel="noopener noreferrer"&gt;Angular Signal Forms Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jdoqocy.com/click-101557355-17135603" rel="noopener noreferrer"&gt;Get a Pluralsight FREE TRIAL HERE!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
