<?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.us-east-2.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 22 hostDirectives De-Duplication Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 03 Jul 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-22-hostdirectives-de-duplication-explained-2gkc</link>
      <guid>https://dev.to/brianmtreese/angular-22-hostdirectives-de-duplication-explained-2gkc</guid>
      <description>&lt;p&gt;&lt;span&gt;H&lt;/span&gt;ost directives are a great way to compose reusable behavior in Angular, but they had one frustrating edge case: two directives could not safely reuse the same shared host directive on the same element. In this post, we'll build a copy button with tooltip behavior, press feedback, and clipboard support, then look at how Angular 22 now de-duplicates shared host directives so this kind of composition works the way you'd expect.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Problem: Shared Behavior Can Collide
&lt;/h2&gt;

&lt;p&gt;In this demo, we have a simple invite link with a button beside 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%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-invite-link.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%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-invite-link.jpg" alt="Example of a simple invite link with a button beside it" width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The button needs three behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On hover, show a tooltip that says "Copy invite link"&lt;/li&gt;
&lt;li&gt;On press, temporarily change the message to "Release to copy"&lt;/li&gt;
&lt;li&gt;After copying, show "Copied!"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly the kind of thing the &lt;a href="https://angular.dev/guide/directives/directive-composition-api?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Directive Composition API&lt;/a&gt; is good at.&lt;/p&gt;

&lt;p&gt;Instead of creating one large directive that handles everything, we can split the behavior into smaller directives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A shared tooltip directive&lt;/li&gt;
&lt;li&gt;A copy button directive&lt;/li&gt;
&lt;li&gt;A pressable directive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That keeps each directive focused, reusable, and easier to reason about.&lt;/p&gt;

&lt;p&gt;But before Angular 22, this setup could break when more than one directive reused the same shared host directive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Shared Tooltip Directive
&lt;/h2&gt;

&lt;p&gt;Let's start with the shared tooltip behavior.&lt;/p&gt;

&lt;p&gt;This directive owns the tooltip message, host attributes, and temporary message 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="k"&gt;import&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="nx"&gt;DestroyRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Directive&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;input&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="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;Directive&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;[appTooltip]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;has-tooltip&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;[attr.aria-label]&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;visibleMessage()&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;[attr.data-tooltip]&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;visibleMessage()&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;[class.show-tooltip]&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;showingTemporaryMessage()&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;TooltipDirective&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;message&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;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;destroyRef&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;DestroyRef&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;temporaryMessage&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;string&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="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&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="nf"&gt;constructor&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;destroyRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDestroy&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;clearTimeout&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;showingTemporaryMessage&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="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="nf"&gt;temporaryMessage&lt;/span&gt;&lt;span class="p"&gt;()&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;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;visibleMessage&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="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="nf"&gt;temporaryMessage&lt;/span&gt;&lt;span class="p"&gt;()&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;message&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&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;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="nf"&gt;clearTimeout&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;temporaryMessage&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;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;clearMessage&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="nf"&gt;clearTimeout&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;temporaryMessage&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="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="nf"&gt;showTemporaryMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&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="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&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="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&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;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temporaryMessage&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="kc"&gt;null&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;timeoutId&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;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&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;if &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;timeoutId&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;span class="nf"&gt;clearTimeout&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;timeoutId&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;timeoutId&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;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 a few important details here.&lt;/p&gt;

&lt;p&gt;The directive has a required &lt;code&gt;message&lt;/code&gt; input using Angular’s &lt;a href="https://angular.dev/api/core/input?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;input()&lt;/a&gt; API.&lt;/p&gt;

&lt;p&gt;It also adds several host bindings:&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="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;has-tooltip&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;[attr.aria-label]&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;visibleMessage()&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;[attr.data-tooltip]&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;visibleMessage()&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;[class.show-tooltip]&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;showingTemporaryMessage()&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 &lt;code&gt;aria-label&lt;/code&gt; gives the button an accessible name.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;data-tooltip&lt;/code&gt; attribute gives the CSS text to render via the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/attr" rel="noopener noreferrer"&gt;attr()&lt;/a&gt; function.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;show-tooltip&lt;/code&gt; lets us force the tooltip to appear when a temporary message is being shown.&lt;/p&gt;

&lt;p&gt;The key point is that this directive does not know anything about copying text or press states.&lt;/p&gt;

&lt;p&gt;It only owns tooltip behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composing Tooltip Behavior Into a Copy Button
&lt;/h2&gt;

&lt;p&gt;Next, we have a directive for the copy 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Directive&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;input&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;TooltipDirective&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;./tooltip&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;Directive&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;[appCopyButton]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hostDirectives&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;directive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputs&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;message: tooltip&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="na"&gt;host&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;(click)&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;copy()&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;CopyButtonDirective&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;copyText&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;copiedMessage&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;copyFailedMessage&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;copiedDuration&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="mi"&gt;2500&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;tooltip&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;TooltipDirective&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;async&lt;/span&gt; &lt;span class="nf"&gt;copy&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="k"&gt;void&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&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;copyText&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;tooltip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showTemporaryMessage&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;copiedMessage&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;copiedDuration&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&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;tooltip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showTemporaryMessage&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;copyFailedMessage&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;copiedDuration&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 directive composes the &lt;code&gt;TooltipDirective&lt;/code&gt; using &lt;code&gt;hostDirectives&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="nx"&gt;hostDirectives&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;directive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputs&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;message: tooltip&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;That means when &lt;code&gt;appCopyButton&lt;/code&gt; is applied to an element, Angular also applies the tooltip behavior to that same host element.&lt;/p&gt;

&lt;p&gt;The directive also exposes the tooltip directive’s &lt;code&gt;message&lt;/code&gt; input as &lt;code&gt;tooltip&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So instead of writing 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;button&lt;/span&gt; &lt;span class="na"&gt;appTooltip&lt;/span&gt; &lt;span class="na"&gt;message=&lt;/span&gt;&lt;span class="s"&gt;"Copy invite link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can expose a more purposeful API through the copy 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;button&lt;/span&gt; &lt;span class="na"&gt;appCopyButton&lt;/span&gt; &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"Copy invite link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, when the button is clicked, the directive writes the text to the clipboard using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText" rel="noopener noreferrer"&gt;Clipboard API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If it works, it temporarily shows the copied message.&lt;/p&gt;

&lt;p&gt;If it fails, it shows the failed message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusing the Same Tooltip in a Pressable Directive
&lt;/h2&gt;

&lt;p&gt;Now let’s add a second behavior directive.&lt;/p&gt;

&lt;p&gt;This one handles the pressed 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Directive&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;input&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;TooltipDirective&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;./tooltip&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;Directive&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;[appPressable]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hostDirectives&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;directive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputs&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;message: tooltip&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="na"&gt;host&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;(pointerdown)&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;press()&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;(pointerup)&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;release()&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;(pointerleave)&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;release()&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;(blur)&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;release()&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;[class.is-pressed]&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;isPressed()&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;PressableDirective&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;pressedMessage&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;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;isPressed&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="kc"&gt;false&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;tooltip&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;TooltipDirective&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;press&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;isPressed&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="kc"&gt;true&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;tooltip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showMessage&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;pressedMessage&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;release&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;isPressed&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="kc"&gt;false&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;tooltip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearMessage&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 directive also composes the same &lt;code&gt;TooltipDirective&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the user presses the button, it sets the pressed state and updates the tooltip message.&lt;/p&gt;

&lt;p&gt;When the user releases, leaves, or blurs the button, it clears the temporary message.&lt;/p&gt;

&lt;p&gt;So now we have two higher-level behavior directives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CopyButtonDirective
  └── TooltipDirective

PressableDirective
  └── TooltipDirective
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the important part.&lt;/p&gt;

&lt;p&gt;Both directives reuse the same lower-level tooltip behavior.&lt;/p&gt;

&lt;p&gt;And that’s where older Angular versions could run into trouble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying Both Directives to the Same Button
&lt;/h2&gt;

&lt;p&gt;In the app component, we import both behavior directives:&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;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;CopyButtonDirective&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;./directives/copy-button&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;PressableDirective&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;./directives/pressable&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-root&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;./app.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;./app.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;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;CopyButtonDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PressableDirective&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;App&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;inviteUrl&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="s1"&gt;https://example.com/articles/angular-host-directives&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;Then, in the template, we layer both behaviors onto the same button:&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;appPressable&lt;/span&gt;
  &lt;span class="na"&gt;pressedMessage=&lt;/span&gt;&lt;span class="s"&gt;"Release to copy"&lt;/span&gt;
  &lt;span class="na"&gt;appCopyButton&lt;/span&gt;
  &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"Copy invite link"&lt;/span&gt;
  &lt;span class="na"&gt;copiedMessage=&lt;/span&gt;&lt;span class="s"&gt;"Copied!"&lt;/span&gt;
  &lt;span class="na"&gt;copyFailedMessage=&lt;/span&gt;&lt;span class="s"&gt;"Unable to copy"&lt;/span&gt;
  &lt;span class="na"&gt;[copyText]=&lt;/span&gt;&lt;span class="s"&gt;"inviteUrl()"&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"icon-button"&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;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;🔗&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&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;This is still just a button.&lt;/p&gt;

&lt;p&gt;But now it has independent behaviors layered onto it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;appPressable&lt;/code&gt; handles press feedback&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;appCopyButton&lt;/code&gt; handles clipboard behavior&lt;/li&gt;
&lt;li&gt;both share the tooltip behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the kind of composition that should be possible with host directives.&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;
  
  
  Why This Used to Break
&lt;/h2&gt;

&lt;p&gt;Before Angular 22, this setup would throw a duplicate directive error:&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%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-duplicate-directive-error.jpg" alt="Example of a duplicate directive error" width="800" height="392"&gt;


&lt;p&gt;The problem was not the button itself.&lt;/p&gt;

&lt;p&gt;The problem was the resolved directive tree.&lt;/p&gt;

&lt;p&gt;Angular saw that &lt;code&gt;appPressable&lt;/code&gt; brought in &lt;code&gt;TooltipDirective&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It also saw that &lt;code&gt;appCopyButton&lt;/code&gt; brought in &lt;code&gt;TooltipDirective&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So from Angular’s point of view, the same directive was being matched more than once on the same element.&lt;/p&gt;

&lt;p&gt;Conceptually, the composition looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;button
  ├── appPressable
  │   └── TooltipDirective
  └── appCopyButton
      └── TooltipDirective
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We were not trying to create two separate tooltips.&lt;/p&gt;

&lt;p&gt;We were trying to compose two higher-level behaviors that happened to share one lower-level behavior.&lt;/p&gt;

&lt;p&gt;That’s a common pattern, especially if you’re building reusable Angular libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Angular 22 De-Duplicates Shared Host Directives
&lt;/h2&gt;

&lt;p&gt;Angular 22 fixes this by de-duplicating shared host directives.&lt;/p&gt;

&lt;p&gt;Now, when the same directive appears more than once in the resolved host directive tree, Angular can merge those matches into a single directive instance.&lt;/p&gt;

&lt;p&gt;So instead of treating this as two separate tooltip directives, Angular recognizes that both composition paths are asking for the same directive.&lt;/p&gt;

&lt;p&gt;Now the button works as expected:&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%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-button-works-as-expected.png" 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%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-button-works-as-expected.png" alt="Example of a button working as expected" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hover shows "Copy invite link"&lt;/li&gt;
&lt;li&gt;Pressing changes the message to "Release to copy"&lt;/li&gt;
&lt;li&gt;Releasing and copying changes the message to "Copied!"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code does not need to change.&lt;/p&gt;

&lt;p&gt;The composition just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Important Caveat: Aliases Still Matter
&lt;/h2&gt;

&lt;p&gt;There is one detail worth calling out.&lt;/p&gt;

&lt;p&gt;Both directives expose the tooltip message input using the same alias:&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="nx"&gt;inputs&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;message: tooltip&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 matters.&lt;/p&gt;

&lt;p&gt;Angular can merge duplicate host directive matches, but it still has to merge their input and output mappings.&lt;/p&gt;

&lt;p&gt;So this is fine:&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="nx"&gt;inputs&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;message: tooltip&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;But you can't do this in the other directive:&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="nx"&gt;inputs&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;message: other-tooltip&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;If one directive exposed the same input as &lt;code&gt;tooltip&lt;/code&gt; and another exposed it as something else, Angular would not know which public API should win.&lt;/p&gt;

&lt;p&gt;That’s when you can run into a conflicting host directive binding error.&lt;/p&gt;

&lt;p&gt;So the rule is simple: If multiple directives reuse the same shared host directive and expose the same input or output, use the same alias.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Makes hostDirectives More Practical
&lt;/h2&gt;

&lt;p&gt;After upgrading to v22, this button can safely combine both higher-level behaviors.&lt;/p&gt;

&lt;p&gt;Angular de-duplicates the shared host directive.&lt;/p&gt;

&lt;p&gt;And the button gets tooltip behavior, press feedback, and clipboard copy behavior without a duplicate directive error.&lt;/p&gt;

&lt;p&gt;That makes directive composition feel a lot more practical.&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;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-host-directives-deduplicate-shared-directives" 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/directives/directive-composition-api?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Directive Composition API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/9c55fcb3e65ffcde32d7ac438ea40a69ffc2b3b6" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/errors/NG8024?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;NG8024: Conflicting Host Directive Binding&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>tutorial</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Angular 22: Better Tab Animations in Material</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/better-angular-material-tab-animations-in-angular-22-3930</link>
      <guid>https://dev.to/brianmtreese/better-angular-material-tab-animations-in-angular-22-3930</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular &lt;a href="https://material.angular.dev/components/tabs/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Material tabs&lt;/a&gt; already make it easy to organize content into separate views, but the animation controls have always had one slightly annoying limitation: the tab indicator and the tab body shared the same animation duration. In Angular 22, that changes. Now, &lt;code&gt;mat-tab-group&lt;/code&gt; can use separate values for the tab header and tab body animation durations, giving us more fine-grained control over the animation.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Problem With Shared Tab Animation Durations
&lt;/h2&gt;

&lt;p&gt;Let's say we have a simple client workspace dashboard:&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%2F06-25%2Fangular-material-tabs-client-dashboard.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%2F06-25%2Fangular-material-tabs-client-dashboard.jpg" alt="Example of a simple client workspace dashboard built using Angular Material tabs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This app uses &lt;a href="https://material.angular.dev/components/tabs/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Material tabs&lt;/a&gt; to separate the client overview, projects, invoices, and recent activity:&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;mat-tab-group&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mat-tab&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Overview"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="nt"&gt;&amp;lt;/mat-tab&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;mat-tab&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Projects"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="nt"&gt;&amp;lt;/mat-tab&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;mat-tab&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Invoices"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="nt"&gt;&amp;lt;/mat-tab&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;mat-tab&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Activity"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="nt"&gt;&amp;lt;/mat-tab&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing fancy here.&lt;/p&gt;

&lt;p&gt;It's just a normal &lt;code&gt;mat-tab-group&lt;/code&gt; with several &lt;code&gt;mat-tab&lt;/code&gt; items inside it.&lt;/p&gt;

&lt;p&gt;Each tab has a label that shows up in the tab header, and each tab also has content that gets displayed in the tab body.&lt;/p&gt;

&lt;p&gt;But when we switch between these tabs, there are actually two different animations happening:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The active tab indicator moves in the tab header&lt;/li&gt;
&lt;li&gt;The tab content slides in the tab body&lt;/li&gt;
&lt;/ol&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%2F06-25%2Fangular-material-tabs-animation-duration.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%2F06-25%2Fangular-material-tabs-animation-duration.jpg" alt="Example of the two different animations that happen when switching between tabs in Angular Material tabs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those two animations are related, but they don't always need the same timing.&lt;/p&gt;

&lt;p&gt;The tab indicator should usually feel quick and responsive because it confirms the user's click.&lt;/p&gt;

&lt;p&gt;The tab body can often move a little slower, especially when the content is heavier or more visual.&lt;/p&gt;

&lt;p&gt;That’s where the old API created a tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Way: One Duration for Everything
&lt;/h2&gt;

&lt;p&gt;Angular Material has supported the &lt;code&gt;animationDuration&lt;/code&gt; input on &lt;code&gt;mat-tab-group&lt;/code&gt; for a long time.&lt;/p&gt;

&lt;p&gt;So, if we wanted to slow down the tab transition, we could do 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;mat-tab-group&lt;/span&gt; &lt;span class="na"&gt;animationDuration=&lt;/span&gt;&lt;span class="s"&gt;"1500ms"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works.&lt;/p&gt;

&lt;p&gt;The tab body would transition much slower, which can make larger content panels feel more intentional.&lt;/p&gt;

&lt;p&gt;But there's a catch.&lt;/p&gt;

&lt;p&gt;That same duration also affects the header indicator animation.&lt;/p&gt;

&lt;p&gt;So the content would feel smooth, but the selected tab indicator would feel like it's dragging behind the click.&lt;/p&gt;

&lt;p&gt;That makes the UI feel less responsive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Indicator Faster Creates the Opposite Problem
&lt;/h2&gt;

&lt;p&gt;We can try to fix the sluggish tab indicator by lowering the duration:&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;mat-tab-group&lt;/span&gt; &lt;span class="na"&gt;animationDuration=&lt;/span&gt;&lt;span class="s"&gt;"150ms"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the selected tab indicator would feel much better.&lt;/p&gt;

&lt;p&gt;It would move quickly, so the user would get immediate feedback.&lt;/p&gt;

&lt;p&gt;But now the tab body animation would be too fast.&lt;/p&gt;

&lt;p&gt;The content would snap into place, and we would lose the smoother transition we wanted in the first place.&lt;/p&gt;

&lt;p&gt;So we would be stuck with a compromise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make the body animation feel smooth, but the header feel slow&lt;/li&gt;
&lt;li&gt;make the header feel fast, but the body feel abrupt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The issue is that the header and body are different parts of the UI.&lt;/p&gt;

&lt;p&gt;They have different jobs.&lt;/p&gt;

&lt;p&gt;So they should be able to have different animation durations.&lt;/p&gt;

&lt;p&gt;And now in Angular 22, they can.&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;
  
  
  Angular 22: Separate Header and Body Durations
&lt;/h2&gt;

&lt;p&gt;Angular Material tabs now allows the &lt;code&gt;animationDuration&lt;/code&gt; input to accept either the original shared duration value or an object with separate &lt;code&gt;header&lt;/code&gt; and &lt;code&gt;body&lt;/code&gt; values.&lt;/p&gt;

&lt;p&gt;So instead of 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;mat-tab-group&lt;/span&gt; &lt;span class="na"&gt;animationDuration=&lt;/span&gt;&lt;span class="s"&gt;"150ms"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can bind to an object:&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;mat-tab-group&lt;/span&gt; &lt;span class="na"&gt;[animationDuration]=&lt;/span&gt;&lt;span class="s"&gt;"{
  header: '150ms',
  body: '1500ms'
}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us much better control.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;header&lt;/code&gt; value controls the tab header animation, which includes the active indicator.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;body&lt;/code&gt; value controls the tab content animation.&lt;/p&gt;

&lt;p&gt;So now the indicator can stay fast, while the body content can transition more slowly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Is Actually Useful
&lt;/h2&gt;

&lt;p&gt;This is a small API improvement, but it solves a real UI polish problem.&lt;/p&gt;

&lt;p&gt;For simple tabs with lightweight content, the default behavior is probably fine.&lt;/p&gt;

&lt;p&gt;But separate durations can help when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tab body contains heavier dashboard content&lt;/li&gt;
&lt;li&gt;you want a more polished transition between larger panels&lt;/li&gt;
&lt;li&gt;the active tab indicator feels delayed&lt;/li&gt;
&lt;li&gt;you want to disable or reduce one animation without affecting the other&lt;/li&gt;
&lt;li&gt;you're trying to make tabs feel more responsive without making the content snap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is that feedback and transition are not always the same thing.&lt;/p&gt;

&lt;p&gt;The header animation is feedback.&lt;/p&gt;

&lt;p&gt;The body animation is transition.&lt;/p&gt;

&lt;p&gt;Now Angular Material lets us tune them separately.&lt;/p&gt;

&lt;p&gt;It's not a huge feature, but it removes a real UI compromise.&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;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-material-tabs-separate-animation-durations" 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/components/commit/85c16fe4bb68d89d1f304350ecf965aeceea4662" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/components/tabs/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Material Tabs 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;Angular Signal Forms Course&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Angular 22: linkedSignal() Can Now Write Back to State</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 19 Jun 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-22-linkedsignal-can-now-write-back-to-state-3eib</link>
      <guid>https://dev.to/brianmtreese/angular-22-linkedsignal-can-now-write-back-to-state-3eib</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular's &lt;a href="https://angular.dev/guide/signals/linked-signal?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;linkedSignal()&lt;/a&gt; just got a small upgrade with a big practical payoff. It can now read from larger signal state and write back to it with a custom setter, giving us a clean field-level API without creating disconnected duplicate state.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; At the time of writing, the feature you're about to see is only available in Angular &lt;code&gt;22.1.0-next.0&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem: Editing Nested Signal State
&lt;/h2&gt;

&lt;p&gt;Let's start with a simple profile settings screen:&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%2F06-18%2Fangular-linked-signal-custom-setter-profile-settings-screen.png" 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%2F06-18%2Fangular-linked-signal-custom-setter-profile-settings-screen.png" alt="Example of a simple profile settings screen in Angular" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have a signed-in user, and a product updates setting that controls how many marketing emails this user can receive each week.&lt;/p&gt;

&lt;p&gt;In the template, we bind the input to a field-level signal &lt;code&gt;maxMarketingEmailsPerWeek()&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;input&lt;/span&gt;
  &lt;span class="na"&gt;#emailLimit&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;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;[value]=&lt;/span&gt;&lt;span class="s"&gt;"maxMarketingEmailsPerWeek()"&lt;/span&gt;
  &lt;span class="na"&gt;(input)=&lt;/span&gt;&lt;span class="s"&gt;"setMaxMarketingEmailsPerWeek(emailLimit.value)"&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 have a couple of buttons that update that value from their own methods &lt;code&gt;sendFewerEmails()&lt;/code&gt; and &lt;code&gt;sendMoreEmails()&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;button&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;[disabled]=&lt;/span&gt;&lt;span class="s"&gt;"maxMarketingEmailsPerWeek() === 0"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"sendFewerEmails()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Send fewer emails
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&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;"button"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"sendMoreEmails()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Send more emails
&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;Nothing too unusual here.&lt;/p&gt;

&lt;p&gt;But the important part is that this value does &lt;strong&gt;not&lt;/strong&gt; live in its own isolated signal.&lt;/p&gt;

&lt;p&gt;The actual source of truth is a larger profile object.&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;profile&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;UserProfile&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;id&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Brian Treese&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="s1"&gt;brian@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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 profile object like this might come from an API, a resource, a store, a parent component, or some other state layer.&lt;/p&gt;

&lt;p&gt;We don't always have a separate signal for every property.&lt;/p&gt;

&lt;p&gt;Sometimes the UI needs to edit one field, while the real state is a larger object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before Angular 22: Reading Was Clean, Writing Wasn't
&lt;/h2&gt;

&lt;p&gt;Before this update, we could use &lt;code&gt;linkedSignal()&lt;/code&gt; to read the field from the parent 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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us a field-level signal for the &lt;code&gt;maxMarketingEmailsPerWeek&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;So the template can read it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;[value]="maxMarketingEmailsPerWeek()"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But writing back to the parent &lt;code&gt;profile&lt;/code&gt; signal still required a separate method.&lt;/p&gt;

&lt;p&gt;First, the input handler received the raw DOM value as a string, converted it to a number, prevented negative values, and then called a &lt;code&gt;updateMaxMarketingEmailsPerWeek()&lt;/code&gt; 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;setMaxMarketingEmailsPerWeek&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="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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nc"&gt;Number&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;||&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;Then that method updated the parent &lt;code&gt;profile&lt;/code&gt; 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="nf"&gt;updateMaxMarketingEmailsPerWeek&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="kr"&gt;number&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;profile&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;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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxMarketingEmailsPerWeek&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The buttons needed to call the same method 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;protected&lt;/span&gt; &lt;span class="nf"&gt;sendFewerEmails&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="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;()&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;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendMoreEmails&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="nf"&gt;updateMaxMarketingEmailsPerWeek&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;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;()&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine.&lt;/p&gt;

&lt;p&gt;But the code is more spread out than it needs to be.&lt;/p&gt;

&lt;p&gt;The linked signal handles the read side, while the write-back logic lives somewhere else.&lt;/p&gt;

&lt;p&gt;That's the part Angular 22 lets us clean up.&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;
  
  
  Angular 22: Add a Custom set() to linkedSignal()
&lt;/h2&gt;

&lt;p&gt;Now &lt;code&gt;linkedSignal()&lt;/code&gt; can take a custom &lt;code&gt;set&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;That means we can keep the read logic and write-back logic 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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;set&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;=&amp;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;profile&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;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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxMarketingEmailsPerWeek&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now this linked signal knows how to read from the profile (which it already did):&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="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;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it also knows how to write back to the profile:&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="kd"&gt;set&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;=&amp;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;profile&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;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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxMarketingEmailsPerWeek&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the key idea.&lt;/p&gt;

&lt;p&gt;The linked signal becomes a writable field-level API that is still backed by the larger profile object.&lt;/p&gt;

&lt;p&gt;This is not a second source of truth and it's not magic two-way binding.&lt;/p&gt;

&lt;p&gt;It's just an explicit write-back function for a focused piece of state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Use set() on the linkedSignal()
&lt;/h2&gt;

&lt;p&gt;Now that the linked signal has custom write behavior, the input handler can be simplified.&lt;/p&gt;

&lt;p&gt;Instead of calling the separate helper method, we can call &lt;code&gt;set()&lt;/code&gt; directly on the linked signal.&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="nf"&gt;setMaxMarketingEmailsPerWeek&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="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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nc"&gt;Number&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;||&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;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="nf"&gt;setMaxMarketingEmailsPerWeek&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="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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nc"&gt;Number&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;||&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;The function still handles the DOM input conversion, but once we have the final value, we just set the field-level signal.&lt;/p&gt;

&lt;p&gt;Because we added the custom setter, that value writes back to the full profile object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Use update() Too
&lt;/h2&gt;

&lt;p&gt;This also works with &lt;code&gt;update()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the button methods can be simplified too.&lt;/p&gt;

&lt;p&gt;Instead of 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="nf"&gt;sendFewerEmails&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="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;()&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;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendMoreEmails&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="nf"&gt;updateMaxMarketingEmailsPerWeek&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;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;()&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can update the linked signal 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="nf"&gt;sendFewerEmails&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;maxMarketingEmailsPerWeek&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;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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;value&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;sendMoreEmails&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;maxMarketingEmailsPerWeek&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;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part I really like.&lt;/p&gt;

&lt;p&gt;Both &lt;code&gt;set()&lt;/code&gt; and &lt;code&gt;update()&lt;/code&gt; flow through the custom setter, so they both write back to the parent &lt;code&gt;profile&lt;/code&gt; signal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;update()&lt;/code&gt; is not bypassing the custom setter.&lt;/p&gt;

&lt;p&gt;It calculates the next value, then sends that value through the same write-back path.&lt;/p&gt;

&lt;p&gt;And now the old &lt;code&gt;updateMaxMarketingEmailsPerWeek()&lt;/code&gt; helper method has no job.&lt;/p&gt;

&lt;p&gt;Always a nice moment.&lt;/p&gt;

&lt;p&gt;We can delete it.&lt;/p&gt;

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

&lt;p&gt;After this change, the component is easier to reason about.&lt;/p&gt;

&lt;p&gt;We have the source of truth:&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;profile&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;UserProfile&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;id&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Brian Treese&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="s1"&gt;brian@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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 field-level linked 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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&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="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;set&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;=&amp;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;profile&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;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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxMarketingEmailsPerWeek&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the UI actions can work directly with that field-level 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="nf"&gt;setMaxMarketingEmailsPerWeek&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="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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nc"&gt;Number&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;||&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;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendFewerEmails&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;maxMarketingEmailsPerWeek&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;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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;value&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;sendMoreEmails&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;maxMarketingEmailsPerWeek&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;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior is the same, but the state management is cleaner because the read and write behavior now live together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This is a small API change, but it's a practical improvement for real Angular state management.&lt;/p&gt;

&lt;p&gt;Before, &lt;code&gt;linkedSignal()&lt;/code&gt; gave us a clean way to read a value from larger state, but writing back to it still needed to happen somewhere else.&lt;/p&gt;

&lt;p&gt;Now the linked signal can own both sides.&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;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-linkedsignal-custom-setter" 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/124ba10ead58c9f93b0b74c4102022c4674db1f5" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/linkedSignal?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular linkedSignal() API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/signals?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Signals Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/" rel="noopener noreferrer"&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>New in Angular 22: Directives for CDK Component Portals</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 12 Jun 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/new-in-angular-22-directives-for-cdk-component-portals-36cj</link>
      <guid>https://dev.to/brianmtreese/new-in-angular-22-directives-for-cdk-component-portals-36cj</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular 22 lets &lt;a href="https://material.angular.dev/cdk/portal/api?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;ComponentPortal&lt;/a&gt; apply directives to a dynamically rendered component’s host element. In this post, we’ll use that to add a custom directive to the same help component rendered through both &lt;a href="https://material.angular.dev/cdk/portal/api?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco#CdkPortalOutlet" rel="noopener noreferrer"&gt;CdkPortalOutlet&lt;/a&gt; and &lt;a href="https://material.angular.dev/cdk/portal/api?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco#DomPortalOutlet" rel="noopener noreferrer"&gt;DomPortalOutlet&lt;/a&gt;.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This example focuses on a few accessibility concepts applied through a directive. It’s not meant to be a complete accessibility pattern for every dynamic panel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem: Dynamic Components Need Host Behavior Too
&lt;/h2&gt;

&lt;p&gt;In this example, we have a simple checkout screen with a “Need help?” area in the sidebar:&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%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel.png" 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%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel.png" alt="A screenshot of the checkout screen with a sidebar and a help area" width="799" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the user clicks the button, we render a payment help 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%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel-rendered.png" 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%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel-rendered.png" alt="A screenshot of the checkout screen after the Need help button is clicked, showing a payment help message rendered in the sidebar" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is done dynamically using the &lt;a href="https://material.angular.dev/cdk/portal/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular CDK Portal&lt;/a&gt; API.&lt;/p&gt;

&lt;p&gt;It works, but currently, there’s a problem.&lt;/p&gt;

&lt;p&gt;The component is rendered dynamically, but the host element doesn’t describe what it is.&lt;/p&gt;

&lt;p&gt;For this example, we’ll expose the help panel as a named region.&lt;/p&gt;

&lt;p&gt;That means it needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;role="region"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An accessible name with &lt;code&gt;aria-label&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A way to receive focus when it opens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gets more interesting because we render the same help component in two different places.&lt;/p&gt;

&lt;p&gt;First, we render it inline in the checkout sidebar with &lt;code&gt;CdkPortalOutlet&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then, we render it again as a floating support panel attached directly to the document body with &lt;code&gt;DomPortalOutlet&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%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel-floating.png" 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%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel-floating.png" alt="A screenshot of the checkout screen with the payment help message rendered as a floating support panel over the page" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same component. Different outlet. Different context.&lt;/p&gt;

&lt;p&gt;So we need context-specific host behavior, but we don’t want to bake that directly into &lt;code&gt;PaymentHelpComponent&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Inline Portal Works
&lt;/h2&gt;

&lt;p&gt;The inline version uses &lt;code&gt;CdkPortalOutlet&lt;/code&gt; in the checkout template.&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;ng-template&lt;/span&gt; &lt;span class="na"&gt;[cdkPortalOutlet]=&lt;/span&gt;&lt;span class="s"&gt;"helpPortal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ng-template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the component, we create the portal only when the help panel should be shown:&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;showHelp&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="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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;helpPortal&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;showHelp&lt;/span&gt;&lt;span class="p"&gt;()&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;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;ComponentPortal&lt;/code&gt; says: “create this Angular component dynamically, and let a portal outlet render it.”&lt;/p&gt;

&lt;p&gt;In this case, the destination is part of the Angular template. &lt;/p&gt;

&lt;p&gt;The help component gets created inside the checkout sidebar.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;DomPortalOutlet&lt;/code&gt; Renders the Floating Portal
&lt;/h2&gt;

&lt;p&gt;The floating version is different.&lt;/p&gt;

&lt;p&gt;Instead of rendering into a template outlet, we create a DOM element manually and attach a portal there.&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="nf"&gt;openSupport&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;hostElement&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&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;hostElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;floating-help&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="nb"&gt;document&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;append&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;hostElement&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;outlet&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;DomPortalOutlet&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;hostElement&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;appRef&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;injector&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;outlet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&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;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PaymentHelpComponent&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;supportOpen&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="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;This is useful when the content should not be constrained by the current component layout.&lt;/p&gt;

&lt;p&gt;The floating support panel is appended to the document body, positioned globally, and managed through the portal outlet.&lt;/p&gt;

&lt;p&gt;So now we have two rendering strategies:&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="c1"&gt;// Inline checkout help&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Floating global help&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both render the same component.&lt;/p&gt;

&lt;p&gt;Both have the same accessibility problem.&lt;/p&gt;

&lt;p&gt;And both need the same host-level behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Directive for the Portal Host
&lt;/h2&gt;

&lt;p&gt;Instead of adding this behavior to the help component itself, we can create a directive.&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;afterNextRender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Directive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ElementRef&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;input&lt;/span&gt;&lt;span class="p"&gt;,&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;Directive&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;[appPortalPanel]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;region&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;[attr.aria-label]&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;ariaLabel()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tabindex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-1&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;PortalPanelDirective&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;ariaLabel&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="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;elementRef&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;ElementRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;afterNextRender&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elementRef&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;focus&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 directive does three things.&lt;/p&gt;

&lt;p&gt;First, it adds the &lt;code&gt;region&lt;/code&gt; role.&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="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;region&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 it binds the &lt;code&gt;aria-label&lt;/code&gt; from an input.&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[attr.aria-label]&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;ariaLabel()&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;And finally, it adds &lt;code&gt;tabindex="-1"&lt;/code&gt; so the panel can receive programmatic focus without adding it to the normal tab order.&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="nx"&gt;tabindex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-1&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;The focus behavior happens after Angular finishes rendering.&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;afterNextRender&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elementRef&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;focus&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 behavior we want, but the important part is where we apply it.&lt;/p&gt;

&lt;p&gt;We don't want to modify &lt;code&gt;PaymentHelpComponent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That component should stay focused on its own content.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;role&lt;/code&gt;. No &lt;code&gt;aria-label&lt;/code&gt;. No focus logic.&lt;/p&gt;

&lt;p&gt;That’s all contextual behavior, so we’ll attach it from the portal instead.&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;
  
  
  Add a Directive to &lt;code&gt;ComponentPortal&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Angular 22 adds directive support to &lt;code&gt;ComponentPortal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means we can apply one or more directives to the dynamically-created component host.&lt;/p&gt;

&lt;p&gt;Here's what it 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;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;helpPortal&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;showHelp&lt;/span&gt;&lt;span class="p"&gt;()&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;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;PaymentHelpComponent&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="kc"&gt;null&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="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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PortalPanelDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment help for checkout&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;span class="p"&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, the constructor call is a little verbose because the new directive support is currently the sixth argument.&lt;/p&gt;

&lt;p&gt;So we pass through the earlier optional arguments:&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;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;PaymentHelpComponent&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="c1"&gt;// viewContainerRef&lt;/span&gt;
  &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// injector&lt;/span&gt;
  &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// projectableNodes&lt;/span&gt;
  &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// bindings&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// Directives&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 key part is the &lt;code&gt;directives&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="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PortalPanelDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment help for checkout&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;The &lt;code&gt;type&lt;/code&gt; tells Angular which directive to apply.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;bindings&lt;/code&gt; array lets us configure that directive.&lt;/p&gt;

&lt;p&gt;In this case, we use &lt;a href="https://angular.dev/api/core/inputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;&lt;code&gt;inputBinding()&lt;/code&gt;&lt;/a&gt; to pass a value into the directive’s &lt;code&gt;ariaLabel&lt;/code&gt; input.&lt;/p&gt;

&lt;p&gt;So when Angular creates &lt;code&gt;PaymentHelpComponent&lt;/code&gt;, it also applies &lt;code&gt;PortalPanelDirective&lt;/code&gt; to the component host.&lt;/p&gt;

&lt;p&gt;That gives us the role, label, tabindex, and focus behavior without changing the help component at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reuse the Same Directive with &lt;code&gt;DomPortalOutlet&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Now we can apply the same idea to the floating support launcher.&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;outlet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&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;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;PaymentHelpComponent&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="kc"&gt;null&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="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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PortalPanelDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Floating payment help&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;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 same component.&lt;/p&gt;

&lt;p&gt;It uses the same directive.&lt;/p&gt;

&lt;p&gt;But now the label is different because the rendering context is different.&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;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Floating payment help&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 the real benefit.&lt;/p&gt;

&lt;p&gt;The component stays reusable, and the portal decides what host behavior makes sense for the place where the component is being rendered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result: Host Behavior Travels with the Portal
&lt;/h2&gt;

&lt;p&gt;After this change, the inline checkout help panel receives the host behavior automatically.&lt;/p&gt;

&lt;p&gt;When it opens, it gets focus.&lt;/p&gt;

&lt;p&gt;And when we inspect the generated host element, it now includes the attributes we need:&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-payment-help&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"region"&lt;/span&gt;
  &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Payment help for checkout"&lt;/span&gt;
  &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/app-payment-help&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The floating panel gets the same behavior too:&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-payment-help&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"region"&lt;/span&gt;
  &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Floating payment help"&lt;/span&gt;
  &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/app-payment-help&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So even though these panels are rendered through different portal outlets, the portal carries the directive configuration with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ComponentPortal&lt;/code&gt; is no longer just about dynamically rendering a component.&lt;/p&gt;

&lt;p&gt;In Angular 22, it can also bring the host behavior that component needs for a specific context.&lt;/p&gt;

&lt;p&gt;That’s especially useful when the same component can appear in multiple places.&lt;/p&gt;

&lt;p&gt;The component stays clean.&lt;/p&gt;

&lt;p&gt;The directive stays reusable.&lt;/p&gt;

&lt;p&gt;And the portal becomes the place where dynamic rendering and context-specific behavior come together.&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;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-cdk-component-portal-directives" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/cdk/portal/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular CDK Portal Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/inputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular inputBinding API&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 Material Dialogs Now Support Real Component Bindings</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 05 Jun 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-material-dialogs-now-support-real-component-bindings-2pmc</link>
      <guid>https://dev.to/brianmtreese/angular-material-dialogs-now-support-real-component-bindings-2pmc</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular &lt;a href="https://material.angular.dev/components/dialog/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Material dialogs&lt;/a&gt; just got a practical upgrade. In Angular 22, dialogs can now bind directly to component inputs, outputs, and model inputs. That means we can build one reusable component, use it inline, and then open that same component inside a dialog without creating a special wrapper component.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;In this demo, we have a reusable &lt;a href="https://github.com/brianmtreese/angular-material-dialog-input-output-model-bindings/tree/main/src/app/notification-settings" rel="noopener noreferrer"&gt;notification settings component&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It needs three things:&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;readonly&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&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="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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;enabled&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="kc"&gt;false&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;saved&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="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So inline, we can use it like 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;app-notification-settings&lt;/span&gt;
  &lt;span class="na"&gt;[user]=&lt;/span&gt;&lt;span class="s"&gt;"user()"&lt;/span&gt;
  &lt;span class="na"&gt;[(enabled)]=&lt;/span&gt;&lt;span class="s"&gt;"notificationsEnabled"&lt;/span&gt;
  &lt;span class="na"&gt;(saved)=&lt;/span&gt;&lt;span class="s"&gt;"saveNotificationSettings($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;This is normal Angular component communication.&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%2F06-04%2Fangular-notification-settings-input-model-output-bindings-inline-component.png" 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%2F06-04%2Fangular-notification-settings-input-model-output-bindings-inline-component.png" alt="An example of the notification settings component with an input, model, and output being used inline in the parent template" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The parent passes in the user, keeps the notification value synced, and listens for the save event.&lt;/p&gt;

&lt;p&gt;But when we open that same component in an Angular Material dialog, things are different:&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;openSettingsDialog&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;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;28rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;panelClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings-dialog&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 component renders, but it is disconnected:&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%2F06-04%2Fangular-material-dialog-notification-settings-component-disconnected-from-parent.png" 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%2F06-04%2Fangular-material-dialog-notification-settings-component-disconnected-from-parent.png" alt="An example of the notification settings component rendered in a dialog, but disconnected from the parent" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It does not receive the &lt;code&gt;user&lt;/code&gt; &lt;a href="https://angular.dev/api/core/input?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;input&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It does not sync the &lt;code&gt;enabled&lt;/code&gt; &lt;a href="https://angular.dev/guide/components/inputs?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco#model-inputs" rel="noopener noreferrer"&gt;model input&lt;/a&gt; value.&lt;/p&gt;

&lt;p&gt;And the parent does not listen for the &lt;code&gt;saved&lt;/code&gt; &lt;a href="https://angular.dev/api/core/output?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;output&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So the goal is simple: use the same component API inside the dialog that we already use inline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reusable Component
&lt;/h2&gt;

&lt;p&gt;Here’s the notification settings component:&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-notification-settings&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;MatButtonModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MatSlideToggleModule&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;./notification-settings.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;./notification-settings.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;NotificationSettingsComponent&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&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="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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;enabled&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="kc"&gt;false&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;saved&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="nx"&gt;NotificationSettingsSaved&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;save&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&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;saved&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="na"&gt;userId&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;user&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;enabled&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;enabled&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;And here’s the template:&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;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"settings-card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Notification Settings&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

    @if (user()) {
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
        Manage email notifications for
        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;{{ user()!.name }}&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;.
      &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    }
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;mat-slide-toggle&lt;/span&gt;
    &lt;span class="na"&gt;[checked]=&lt;/span&gt;&lt;span class="s"&gt;"enabled()"&lt;/span&gt;
    &lt;span class="na"&gt;(change)=&lt;/span&gt;&lt;span class="s"&gt;"enabled.set($event.checked)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Email notifications
  &lt;span class="nt"&gt;&amp;lt;/mat-slide-toggle&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;matButton=&lt;/span&gt;&lt;span class="s"&gt;"filled"&lt;/span&gt; &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"save()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Save Settings
  &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing here is that this component knows nothing about dialogs.&lt;/p&gt;

&lt;p&gt;It does not inject &lt;a href="https://material.angular.dev/components/dialog/api?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco#MAT_DIALOG_DATA" rel="noopener noreferrer"&gt;MAT_DIALOG_DATA&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It does not need &lt;a href="https://material.angular.dev/components/dialog/api?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco#MatDialogRef" rel="noopener noreferrer"&gt;MatDialogRef&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is just a regular Angular component with inputs, a model input, and an output.&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;
  
  
  Add Dialog Bindings
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/angular/components/commit/bf3596b53ba1cf118ec06343f8a7772e0fb0e55d" rel="noopener noreferrer"&gt;Angular 22&lt;/a&gt;, &lt;code&gt;MatDialog&lt;/code&gt; supports a new &lt;code&gt;bindings&lt;/code&gt; array.&lt;/p&gt;

&lt;p&gt;This lets us bind to the component that gets rendered inside the dialog using &lt;a href="https://angular.dev/api/core/inputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;inputBinding()&lt;/a&gt;, &lt;a href="https://angular.dev/api/core/outputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;outputBinding()&lt;/a&gt;, and &lt;a href="https://angular.dev/api/core/twoWayBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;twoWayBinding()&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="nx"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;outputBinding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;twoWayBinding&lt;/span&gt;&lt;span class="p"&gt;,&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="nf"&gt;openSettingsDialog&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;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;28rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;panelClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&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;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;outputBinding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;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;saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveNotificationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nf"&gt;twoWayBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&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;notificationsEnabled&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;h3&gt;
  
  
  What Each Binding Does
&lt;/h3&gt;

&lt;p&gt;This passes the current user into the dialog component:&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;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is the programmatic version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;[user]="user()"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This listens for the save event:&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="nx"&gt;outputBinding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;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;saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveNotificationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&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;It is the programmatic version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;(saved)="saveNotificationSettings($event)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this keeps the model input synced:&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;twoWayBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&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;notificationsEnabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is the programmatic version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;[(enabled)]="notificationsEnabled"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now the dialog component receives the user, starts with the correct notification value, updates the parent signal when the toggle changes, and emits the same save event as the inline version.&lt;/p&gt;

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

&lt;p&gt;Now we can use the same component in both places.&lt;/p&gt;

&lt;p&gt;Inline:&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-notification-settings&lt;/span&gt;
  &lt;span class="na"&gt;[user]=&lt;/span&gt;&lt;span class="s"&gt;"user()"&lt;/span&gt;
  &lt;span class="na"&gt;[(enabled)]=&lt;/span&gt;&lt;span class="s"&gt;"notificationsEnabled"&lt;/span&gt;
  &lt;span class="na"&gt;(saved)=&lt;/span&gt;&lt;span class="s"&gt;"saveNotificationSettings($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;&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%2F06-04%2Fangular-notification-settings-input-model-output-bindings-final-inline-component.png" 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%2F06-04%2Fangular-notification-settings-input-model-output-bindings-final-inline-component.png" alt="An example of the notification settings component with an input, model, and output being used inline in the parent template" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And inside a dialog:&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;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;28rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;panelClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&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;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;outputBinding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;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;saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveNotificationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;twoWayBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&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;notificationsEnabled&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;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%2F06-04%2Fangular-material-dialog-notification-settings-input-model-output-bindings.png" 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%2F06-04%2Fangular-material-dialog-notification-settings-input-model-output-bindings.png" alt="An example of the notification settings component with an input, model, and output being used inside a dialog" width="799" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No wrapper component.&lt;/p&gt;

&lt;p&gt;No dialog-specific API.&lt;/p&gt;

&lt;p&gt;No manual component instance wiring.&lt;/p&gt;

&lt;p&gt;Just one reusable component with a clean Angular API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This is a small feature, but it makes dynamic components feel much more like components used directly in a template.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;inputBinding()&lt;/code&gt;, &lt;code&gt;outputBinding()&lt;/code&gt;, and &lt;code&gt;twoWayBinding()&lt;/code&gt;, Angular Material dialogs can now work with the same component contracts we already use everywhere else.&lt;/p&gt;

&lt;p&gt;Build the component normally, then let the dialog bind to it.&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;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-material-dialog-input-output-model-bindings" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/components/dialog/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Material Dialog Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/inputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular &lt;code&gt;inputBinding()&lt;/code&gt; API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/outputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular &lt;code&gt;outputBinding()&lt;/code&gt; API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/twoWayBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular &lt;code&gt;twoWayBinding()&lt;/code&gt; API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Angular @switch Upgrades You Should Know About</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 29 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/the-angular-switch-upgrades-you-should-know-about-102c</link>
      <guid>https://dev.to/brianmtreese/the-angular-switch-upgrades-you-should-know-about-102c</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular's &lt;a href="https://angular.dev/api/core/@switch?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;@switch&lt;/a&gt; block has become a lot more useful recently. With exhaustive type checking, Angular can now catch missing template states when a &lt;a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types" rel="noopener noreferrer"&gt;TypeScript union&lt;/a&gt; or &lt;a href="https://www.typescriptlang.org/docs/handbook/enums.html" rel="noopener noreferrer"&gt;enum&lt;/a&gt; changes. And with grouped cases, we can remove duplicate markup when multiple states render the same UI. In this post, I'll show both improvements using a real-world example.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Problem: UI States Can Drift from Your Types
&lt;/h2&gt;

&lt;p&gt;Let's start with a common pattern.&lt;/p&gt;

&lt;p&gt;We have a support queue where each ticket has a status:&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-28%2Fsupport-queue.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-28%2Fsupport-queue.jpg" alt="Support queue showing tickets with New, In Progress, Resolved, and Closed status labels" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are four possible statuses, and we’re modeling them with a TypeScript union:&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TicketStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in-progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&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;Ticket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&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="nl"&gt;title&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;customer&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TicketStatus&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 containing a list of tickets:&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;tickets&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;Ticket&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User cannot reset password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme Corp&lt;/span&gt;&lt;span class="dl"&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;new&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Billing page shows incorrect total&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Northstar Health&lt;/span&gt;&lt;span class="dl"&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;in-progress&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Exported report is missing rows&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Velocity Labs&lt;/span&gt;&lt;span class="dl"&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;resolved&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1027&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Login button disabled after failed attempt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summit Bank&lt;/span&gt;&lt;span class="dl"&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;closed&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1028&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Production checkout error for enterprise account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Atlas Retail&lt;/span&gt;&lt;span class="dl"&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;new&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;So far, this is simple enough.&lt;/p&gt;

&lt;p&gt;The status is strongly typed, and every ticket has to use one of the values from the &lt;code&gt;TicketStatus&lt;/code&gt; union.&lt;/p&gt;

&lt;p&gt;Now let's look at the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Basic Angular &lt;a class="mentioned-user" href="https://dev.to/switch"&gt;@switch&lt;/a&gt; Block
&lt;/h2&gt;

&lt;p&gt;In the template, we're looping over the tickets with &lt;a href="https://angular.dev/api/core/@for?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;@for&lt;/a&gt; and rendering each one as a card.&lt;/p&gt;

&lt;p&gt;Inside each card, we use an Angular &lt;code&gt;@switch&lt;/code&gt; block to render the status badge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@switch (ticket.status) {
  @case ('new') {
    &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;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @case ('in-progress') {
    &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;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @case ('resolved') {
    &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;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @case ('closed') {
    &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;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, a little lower in the card, we have another &lt;code&gt;@switch&lt;/code&gt; block for the status message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@switch (ticket.status) {
  @case ('new') {
    &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;"status-message active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      This ticket is new and needs to be triaged.
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  }
  @case ('in-progress') {
    &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;"status-message active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Someone is actively working on this issue.
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  }
  @case ('resolved') {
    &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;"status-message done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      The issue has been resolved and is waiting for confirmation.
    &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  }
  @case ('closed') {
    &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;"status-message done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      This ticket is closed and no further action is needed.
    &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 works, but there's a subtle problem.&lt;/p&gt;

&lt;p&gt;Neither &lt;code&gt;@switch&lt;/code&gt; block has a default case.&lt;/p&gt;

&lt;p&gt;That means if the status ever becomes something other than these four values, Angular won't render anything for that part of the UI.&lt;/p&gt;

&lt;p&gt;No badge.&lt;/p&gt;

&lt;p&gt;No message.&lt;/p&gt;

&lt;p&gt;Just a quiet little bug.&lt;/p&gt;

&lt;p&gt;And those are the worst kind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a New Union Value
&lt;/h2&gt;

&lt;p&gt;Now let's say the product changes.&lt;/p&gt;

&lt;p&gt;We need to support urgent tickets, so we add a new status called "escalated":&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TicketStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in-progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;escalated&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 update one of the tickets to use the new status:&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1028&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Production checkout error for enterprise account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Atlas Retail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&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;escalated&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;TypeScript is happy.&lt;/p&gt;

&lt;p&gt;The app still compiles.&lt;/p&gt;

&lt;p&gt;And at first glance, everything looks fine.&lt;/p&gt;

&lt;p&gt;But when the escalated ticket renders, the card is incomplete:&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-28%2Fescalated-ticket.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-28%2Fescalated-ticket.jpg" alt="Escalated ticket showing the ticket title and customer, but no badge or message" width="800" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ticket title and customer show up, but the badge and message are missing because neither &lt;code&gt;@switch&lt;/code&gt; block handles the new value.&lt;/p&gt;

&lt;p&gt;The type changed.&lt;/p&gt;

&lt;p&gt;The data changed.&lt;/p&gt;

&lt;p&gt;But the template didn't keep up.&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;
  
  
  Exhaustive &lt;a class="mentioned-user" href="https://dev.to/switch"&gt;@switch&lt;/a&gt; Checking with &lt;a class="mentioned-user" href="https://dev.to/default"&gt;@default&lt;/a&gt; never
&lt;/h2&gt;

&lt;p&gt;This is where the newer &lt;code&gt;@switch&lt;/code&gt; behavior helps.&lt;/p&gt;

&lt;p&gt;Instead of adding a normal fallback UI, we can add this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@default never;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Angular that if the &lt;code&gt;@switch&lt;/code&gt; reaches the default case, there should be no possible value left to handle.&lt;/p&gt;

&lt;p&gt;So the badge &lt;code&gt;@switch&lt;/code&gt; becomes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@switch (ticket.status) {
  @case ('new') {
    &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;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @case ('in-progress') {
    &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;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @case ('resolved') {
    &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;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @case ('closed') {
    &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;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @default never;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Angular can type-check the &lt;code&gt;@switch&lt;/code&gt; exhaustively.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;TicketStatus&lt;/code&gt; includes &lt;code&gt;escalated&lt;/code&gt;, but the template doesn't handle it yet, Angular reports an 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%2F05-28%2Fescalated-ticket-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%2F05-28%2Fescalated-ticket-error.jpg" alt="Escalated ticket error showing the template error message" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the win.&lt;/p&gt;

&lt;p&gt;Instead of silently rendering broken UI, Angular forces us to update the template when the union type changes.&lt;/p&gt;

&lt;p&gt;This is especially useful for UI states like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ticket statuses&lt;/li&gt;
&lt;li&gt;order states&lt;/li&gt;
&lt;li&gt;payment states&lt;/li&gt;
&lt;li&gt;deployment states&lt;/li&gt;
&lt;li&gt;user invite states&lt;/li&gt;
&lt;li&gt;feature flag states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any time a known set of values drives intentional UI, exhaustive checking is worth considering.&lt;/p&gt;

&lt;p&gt;To fix the error, we just need to add the missing &lt;code&gt;escalated&lt;/code&gt; case in both &lt;code&gt;@switch&lt;/code&gt; blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning Up Duplicate &lt;a class="mentioned-user" href="https://dev.to/case"&gt;@case&lt;/a&gt; Blocks
&lt;/h2&gt;

&lt;p&gt;Now that the &lt;code&gt;@switch&lt;/code&gt; is safer, let's make it cleaner.&lt;/p&gt;

&lt;p&gt;In the badge &lt;code&gt;@switch&lt;/code&gt;, &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;in-progress&lt;/code&gt; both render the same badge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@case ('new') {
  &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;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
@case ('in-progress') {
  &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;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;resolved&lt;/code&gt; and &lt;code&gt;closed&lt;/code&gt; both render the same badge too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@case ('resolved') {
  &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;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
@case ('closed') {
  &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;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That duplication isn't terrible in a small example, but it gets annoying fast in real templates.&lt;/p&gt;

&lt;p&gt;Modern Angular lets us combine consecutive cases that render the same block.&lt;/p&gt;

&lt;p&gt;So we can rewrite the badge &lt;code&gt;@switch&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@switch (ticket.status) {
  @case ('new')
  @case ('in-progress') {
    &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;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @case ('resolved')
  @case ('closed') {
    &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;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @case ('escalated') {
    &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;"badge escalated"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Escalated&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  }
  @default never;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular treats those consecutive &lt;code&gt;@case&lt;/code&gt; statements as multiple conditions for the same template block.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;in-progress&lt;/code&gt; both render the &lt;code&gt;Active&lt;/code&gt; badge, while &lt;code&gt;resolved&lt;/code&gt; and &lt;code&gt;closed&lt;/code&gt; both render the &lt;code&gt;Done&lt;/code&gt; badge.&lt;/p&gt;

&lt;p&gt;We get the same UI, but without repeating the same markup in multiple cases.&lt;/p&gt;

&lt;p&gt;Fewer places to forget something later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safer Templates, Cleaner Code
&lt;/h2&gt;

&lt;p&gt;That’s the real upgrade.&lt;/p&gt;

&lt;p&gt;The UI stays the same, but the template becomes safer, cleaner, and easier to maintain.&lt;/p&gt;

&lt;p&gt;Exhaustive checking helps Angular catch missing states, and grouped cases help remove duplicate markup.&lt;/p&gt;

&lt;p&gt;Small change, safer template.&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;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-switch-block-updates" 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/api/core/%40switch?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular @switch API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/templates/control-flow?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Control Flow 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>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Better Loading Buttons in Angular Material v22</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 22 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/better-loading-buttons-in-angular-material-v22-17b7</link>
      <guid>https://dev.to/brianmtreese/better-loading-buttons-in-angular-material-v22-17b7</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular Material v22 adds a small but surprisingly useful improvement to buttons: &lt;a href="https://github.com/angular/components/commit/b4a89d5996864e591cfac762db420ec591d931e2" rel="noopener noreferrer"&gt;built-in progress indicator support&lt;/a&gt;. Instead of manually swapping button text with a spinner and dealing with layout jumpiness, we can let the &lt;a href="https://v9.material.angular.dev/components/button/overview" rel="noopener noreferrer"&gt;Material button directive&lt;/a&gt; manage the loading UI for us. In this post, I'll show you the old manual approach, why it creates a small UX issue, and how Angular Material v22 cleans it up.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  A Simple Example
&lt;/h2&gt;

&lt;p&gt;Let's start with a simple reports page:&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-21%2Freports-page.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-21%2Freports-page.jpg" alt="A simple reports page with a list of reports and a download button for each report" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have a list of reports, and each report has a "Download" button using the &lt;a href="https://material.angular.dev/components/button/overview" rel="noopener noreferrer"&gt;matButton&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;section&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;"report-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    @for (report of reports(); track report.id) {
      &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;"report-row"&lt;/span&gt;&lt;span class="nt"&gt;&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;"report-info"&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;"report-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{% raw %}{{ report.name }}{% endraw %}&lt;span class="nt"&gt;&amp;lt;/span&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;"report-meta"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{% raw %}{{ report.date }} · {{ report.size }}{% endraw %}&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;

        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
          &lt;span class="na"&gt;matButton=&lt;/span&gt;&lt;span class="s"&gt;"outlined"&lt;/span&gt;
          &lt;span class="na"&gt;[disabled]=&lt;/span&gt;&lt;span class="s"&gt;"downloadingId() !== null"&lt;/span&gt;
          &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"download(report)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          Download
        &lt;span class="nt"&gt;&amp;lt;/button&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;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing unusual here.&lt;/p&gt;

&lt;p&gt;But the loading UI is where this usually gets a little clunky.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Way: Swap the Label for a Spinner
&lt;/h2&gt;

&lt;p&gt;Before this new Angular Material v22 feature, a common approach was to conditionally replace the button label with a spinner.&lt;/p&gt;

&lt;p&gt;First, we need to import the &lt;a href="https://material.angular.dev/components/progress-spinner/overview" rel="noopener noreferrer"&gt;progress spinner&lt;/a&gt; in our component:&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;MatProgressSpinner&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/material/progress-spinner&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-report-list&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;./report-list.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;./report-list.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="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;MatButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MatProgressSpinner&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;ReportListComponent&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;Then we can update the button template to include the progress spinner conditionally when the report is downloading and the label when it isn't:&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;matButton=&lt;/span&gt;&lt;span class="s"&gt;"outlined"&lt;/span&gt;
  &lt;span class="na"&gt;[disabled]=&lt;/span&gt;&lt;span class="s"&gt;"downloadingId() !== null"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"download(report)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  @if (downloadingId() === report.id) {
    &lt;span class="nt"&gt;&amp;lt;mat-progress-spinner&lt;/span&gt;
      &lt;span class="na"&gt;mode=&lt;/span&gt;&lt;span class="s"&gt;"indeterminate"&lt;/span&gt;
      &lt;span class="na"&gt;[diameter]=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt;
      &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Downloading"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  } @else {
    Download
  }
&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;Since this download doesn't expose a real percentage, &lt;code&gt;mode="indeterminate"&lt;/code&gt; is the right fit here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;diameter&lt;/code&gt; keeps the spinner small enough to fit inside the button, and the &lt;code&gt;aria-label&lt;/code&gt; gives assistive technologies meaningful loading-state text since the visible label is being replaced.&lt;/p&gt;

&lt;p&gt;And this works fine:&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-21%2Freports-page-downloading.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%2F05-21%2Freports-page-downloading.gif" alt="A reports page with a list of reports and a download button for each report. When the report is downloading, we show a spinner. When it isn't, we show the label." width="760" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the report is downloading, we show a spinner. &lt;/p&gt;

&lt;p&gt;When it isn't, we show the label.&lt;/p&gt;

&lt;p&gt;But there's still a UX issue.&lt;/p&gt;

&lt;p&gt;The button shrinks when the label disappears and only the spinner remains.&lt;/p&gt;

&lt;p&gt;That's because we're swapping the button's content entirely. &lt;/p&gt;

&lt;p&gt;The text has one width, the spinner has another, and the button resizes to fit whatever is currently rendered.&lt;/p&gt;

&lt;p&gt;It's not broken, but it feels a little janky.&lt;/p&gt;

&lt;p&gt;And we had to write the conditional content logic ourselves.&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 Angular Material v22 Way: Use &lt;code&gt;showProgress&lt;/code&gt; and &lt;code&gt;progressIndicator&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Angular Material v22 gives us another option.&lt;/p&gt;

&lt;p&gt;Instead of swapping the label and spinner manually, both can live inside the button at the same time:&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;matButton=&lt;/span&gt;&lt;span class="s"&gt;"outlined"&lt;/span&gt;
  &lt;span class="na"&gt;[showProgress]=&lt;/span&gt;&lt;span class="s"&gt;"downloadingId() === report.id"&lt;/span&gt;
  &lt;span class="na"&gt;[disabled]=&lt;/span&gt;&lt;span class="s"&gt;"downloadingId() !== null"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"download(report)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mat-progress-spinner&lt;/span&gt;
    &lt;span class="na"&gt;progressIndicator&lt;/span&gt;
    &lt;span class="na"&gt;mode=&lt;/span&gt;&lt;span class="s"&gt;"indeterminate"&lt;/span&gt;
    &lt;span class="na"&gt;[diameter]=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt;
    &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Downloading"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  Download
&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;There are two important pieces here.&lt;/p&gt;

&lt;p&gt;First, the button gets the &lt;code&gt;showProgress&lt;/code&gt; input, which is new in Angular Material v22:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;[showProgress]="downloadingId() === report.id"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the &lt;code&gt;matButton&lt;/code&gt; directive when the progress UI should be shown.&lt;/p&gt;

&lt;p&gt;In this case, we only want progress on the button for the report currently being downloaded.&lt;/p&gt;

&lt;p&gt;Then, the spinner gets the &lt;code&gt;progressIndicator&lt;/code&gt; attribute:&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;mat-progress-spinner&lt;/span&gt; &lt;span class="na"&gt;progressIndicator&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 marks the spinner as the button's projected progress indicator.&lt;/p&gt;

&lt;p&gt;So instead of us writing an &lt;code&gt;@if&lt;/code&gt; block to decide what appears, Angular Material controls that progress indicator slot for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Feels Better
&lt;/h2&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-21%2Freports-page-downloading-with-progress.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%2F05-21%2Freports-page-downloading-with-progress.gif" alt="A reports page with a list of reports and a download button for each report. When the report is downloading, we show a spinner. When it isn't, we show the label, this time using the new showProgress input." width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important detail is that the normal button content stays in the layout, even when the progress indicator is visible.&lt;/p&gt;

&lt;p&gt;So the button still knows how wide the "Download" label is, even while the spinner is being displayed.&lt;/p&gt;

&lt;p&gt;That means we avoid the width jump caused by replacing the content completely.&lt;/p&gt;

&lt;p&gt;The end result is a loading button that feels stable instead of jumpy.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Does Not Have to Be a Material Spinner
&lt;/h2&gt;

&lt;p&gt;One nice part of this API is that &lt;code&gt;progressIndicator&lt;/code&gt; is a projection slot.&lt;/p&gt;

&lt;p&gt;That means the projected content doesn't have to be &lt;code&gt;mat-progress-spinner&lt;/code&gt; specifically.&lt;/p&gt;

&lt;p&gt;You could use a custom loading element if that fits your design system better.&lt;/p&gt;

&lt;p&gt;The main thing is to make sure the progress indicator still communicates meaningful loading-state information, especially when the visible button label is hidden or visually replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaner Loading Buttons in Angular Material
&lt;/h2&gt;

&lt;p&gt;This is one of those Angular Material updates that isn't huge, but it's immediately useful.&lt;/p&gt;

&lt;p&gt;The old approach works, but it usually means manually swapping content and accepting small layout issues.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;showProgress&lt;/code&gt; and &lt;code&gt;progressIndicator&lt;/code&gt;, Angular Material gives us a built-in pattern for loading buttons that feels more polished with less template logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to Go Deeper With Modern Angular?
&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;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-material-button-progress-indicator" 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/components/commit/b4a89d5996864e591cfac762db420ec591d931e2" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/components/button/overview" rel="noopener noreferrer"&gt;Angular Material Button Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/components/progress-spinner/overview" rel="noopener noreferrer"&gt;Angular Material Progress Spinner 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>tutorial</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Angular v22 WebMCP Tools Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 15 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-v22-webmcp-tools-explained-3o9f</link>
      <guid>https://dev.to/brianmtreese/angular-v22-webmcp-tools-explained-3o9f</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular v22 is experimenting with a new way to expose your app’s real capabilities to AI. Instead of letting models guess what's happening by scraping the DOM, we can now provide explicit, state-backed tools directly to the browser. This post walks through setting up &lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; tools to bridge the gap between your Angular state and AI models like &lt;a href="https://gemini.google.com/" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt;.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Problem: AI Is Blind to App State
&lt;/h2&gt;

&lt;p&gt;Most AI interactions with web apps today are limited to what the model can “see” in the rendered page.&lt;/p&gt;

&lt;p&gt;This is brittle, surface-level, and completely misses the rich business logic living inside our services and signals.&lt;/p&gt;

&lt;p&gt;Here's an example of a basic Angular app with no WebMCP tools registered:&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-14%2Fbasic-angular-app-not-using-webmcp-tools.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-14%2Fbasic-angular-app-not-using-webmcp-tools.jpg" alt="Basic Angular app not using WebMCP tools" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're using the &lt;a href="https://chromewebstore.google.com/detail/WebMCP%20-%20Model%20Context%20Tool%20Inspector/gbpdfapgefenggkahomfgkhfehlcenpd" rel="noopener noreferrer"&gt;WebMCP Inspector&lt;/a&gt; to see what tools are available and in this case, there are none:&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-14%2Fwebmcp-inspector-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%2F05-14%2Fwebmcp-inspector-empty.jpg" alt="WebMCP Inspector showing no tools available" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this app, we have data that doesn’t always live in the DOM, like retention risk calculations and hidden account metrics, that could help an AI provide much better assistance.&lt;/p&gt;

&lt;p&gt;Without a structured way to expose these capabilities, the AI is just left guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Angular Signal State
&lt;/h2&gt;

&lt;p&gt;Before we can give the AI tools, we need a clean source of truth. &lt;/p&gt;

&lt;p&gt;In this app, we're using a &lt;code&gt;UserStore&lt;/code&gt; service to manage our customer data.&lt;/p&gt;

&lt;p&gt;We're using the new &lt;a href="https://dev.to%20post_url%20/2026/04/2026-04-30-angular-service-decorator-injectable-replacement%20"&gt;@Service()&lt;/a&gt; decorator and &lt;a href="https://angular.dev/guide/signals" rel="noopener noreferrer"&gt;signals&lt;/a&gt; to expose profile information and computed business 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;import&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="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;USER_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserKey&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;./user.mock-data&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;UserStore&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;selectedUserKeyState&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;UserKey&lt;/span&gt;&lt;span class="o"&gt;&amp;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;sarah&lt;/span&gt;&lt;span class="dl"&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;users&lt;/span&gt; &lt;span class="o"&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sarah&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;USER_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sarah&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marcus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;USER_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marcus&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="p"&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="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;selectedUserKey&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;selectedUserKeyState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asReadonly&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;currentUser&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="nx"&gt;USER_ACCOUNTS&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;selectedUserKey&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;readonly&lt;/span&gt; &lt;span class="nx"&gt;account&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="nx"&gt;USER_ACCOUNTS&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;selectedUserKey&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;readonly&lt;/span&gt; &lt;span class="nx"&gt;isHighRiskAccount&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&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;account&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="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysSinceLastLogin&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentFailures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unresolvedPriorityTickets&lt;/span&gt; &lt;span class="o"&gt;&amp;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;span class="nf"&gt;selectUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserKey&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;selectedUserKeyState&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;userKey&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 &lt;code&gt;isHighRiskAccount&lt;/code&gt; signal is exactly the kind of "hidden" logic that an AI wouldn't know about just by looking at the UI. &lt;/p&gt;

&lt;p&gt;It provides a clear, computed state based on multiple factors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating WebMCP Tools with provideWebMcpTools()
&lt;/h2&gt;

&lt;p&gt;Angular v22 introduces &lt;code&gt;provideWebMcpTools()&lt;/code&gt;, a new API that registers WebMCP tools through the provider system.&lt;/p&gt;

&lt;p&gt;This is powerful because these tools run inside the Angular injection context, meaning they can use dependency injection to access our services and signals.&lt;/p&gt;

&lt;p&gt;Since this is still experimental, I’d treat this as a preview of where Angular and browser-based AI tooling are heading rather than a production recommendation today.&lt;/p&gt;

&lt;p&gt;In this case, we register our tools globally in &lt;code&gt;app.config.ts&lt;/code&gt;, but you should also be able to use it in route providers 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;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;provideWebMcpTools&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApplicationConfig&lt;/span&gt; &lt;span class="o"&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="nf"&gt;provideWebMcpTools&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;Each tool needs a &lt;code&gt;name&lt;/code&gt;, a &lt;code&gt;description&lt;/code&gt; for the AI to understand when to use it, an &lt;code&gt;inputSchema&lt;/code&gt;, and an &lt;code&gt;execute&lt;/code&gt; function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool 1: Summarize the Current Customer
&lt;/h3&gt;

&lt;p&gt;This tool provides a basic summary of the currently selected user. &lt;/p&gt;

&lt;p&gt;It's intentionally simple to demonstrate the core wiring: Gemini asks for a summary, and Angular returns the current signal 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="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_current_user_summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Returns a summary of the current user.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&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;execute&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;user&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;UserStore&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;currentUser&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;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;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Name: &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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Email: &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="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Plan: &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="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Status: &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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Last login: &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="nx"&gt;lastLogin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Open tickets: &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="nx"&gt;openTickets&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="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;Notice how the &lt;code&gt;execute&lt;/code&gt; function directly injects &lt;code&gt;UserStore&lt;/code&gt; and reads the &lt;code&gt;currentUser&lt;/code&gt; signal. &lt;/p&gt;

&lt;p&gt;The AI isn't scraping the DOM, it's calling a tool that accesses real Angular application state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool 2: Analyze Retention Risk
&lt;/h3&gt;

&lt;p&gt;This tool analyzes the current account and explains its retention risk. &lt;/p&gt;

&lt;p&gt;It leverages the &lt;code&gt;isHighRiskAccount&lt;/code&gt; computed signal we defined earlier.&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_retention_risk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Analyzes the current account and explains retention risk.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&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;execute&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;store&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;UserStore&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentUser&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;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&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;isHighRisk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHighRiskAccount&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;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;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isHighRisk&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;
            &lt;span class="c1"&gt;// Is high risk&lt;/span&gt;
            &lt;span class="s2"&gt;`&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is currently considered high risk.

            Reasons:
            - &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="nx"&gt;daysSinceLastLogin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; days since last login
            - &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="nx"&gt;paymentFailures&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; recent payment failures
            - &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="nx"&gt;unresolvedPriorityTickets&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; unresolved priority ticket

            Recommended next action:                  
            Have customer success schedule a live onboarding session.`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;

            &lt;span class="c1"&gt;// Is low risk&lt;/span&gt;
            &lt;span class="s2"&gt;`&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is currently considered low risk.

            No significant warning signs detected.

            Recommended next action:
            No immediate action needed.`&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 demonstrates how the AI can use app-defined business logic to determine a customer’s risk level, providing actionable insights based on our application's internal state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool 3: Draft a Customer Success Note
&lt;/h3&gt;

&lt;p&gt;The third tool drafts a customer success outreach note.&lt;/p&gt;

&lt;p&gt;The message content dynamically changes based on whether the &lt;code&gt;isHighRiskAccount&lt;/code&gt; signal is true or false.&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;name&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_customer_success_note&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Drafts a customer success outreach note for the current account.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&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;execute&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;store&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;UserStore&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentUser&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;isHighRisk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHighRiskAccount&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;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;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isHighRisk&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; 
            &lt;span class="c1"&gt;// Is high risk&lt;/span&gt;
            &lt;span class="s2"&gt;`Hi &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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,

            I noticed you may have run into a few issues recently, and I wanted to reach out personally to see if we can help.

            If you'd like, we can schedule a quick onboarding session to walk through the platform and help resolve any blockers.

            Thanks,
            Customer Success Team`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; 

            &lt;span class="c1"&gt;// Is low risk&lt;/span&gt;
            &lt;span class="s2"&gt;`Hi &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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,

            Just wanted to check in and make sure everything is still going smoothly.

            Thanks for being a customer, and let us know if there’s anything we can help with.

            Thanks,
            Customer Success Team`&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 tool showcases how the same underlying Angular state can power multiple AI-callable capabilities, leading to context-aware and personalized AI responses.&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;
  
  
  Testing with the Inspector
&lt;/h3&gt;

&lt;p&gt;Once registered, all three tools appear in the WebMCP Model Context Tool Inspector:&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%2F05-14%2Fwebmcp-inspector-tools.jpg" alt="Gemini using the registered WebMCP tools" width="800" height="639"&gt;

&lt;p&gt;This allows the AI to see exactly what capabilities our app provides.&lt;/p&gt;

&lt;p&gt;We can also see that Gemini already understands the tools available and has filled out the prompt for us:&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%2F05-14%2Fgemini-understanding-tools.jpg" alt="Gemini understanding the tools available" width="800" height="313"&gt;

&lt;p&gt;When we ask Gemini questions like "Can you summarize this customer account?": &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%2F05-14%2Fgemini-summarize-customer-account.jpg" alt="Gemini summarizing the customer account" width="800" height="414"&gt;

&lt;p&gt;or "Is this customer at risk?": &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%2F05-14%2Fgemini-customer-at-risk.jpg" alt="Gemini checking if the customer is at risk" width="800" height="476"&gt;

&lt;p&gt;or "Draft a follow-up message for this customer":&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%2F05-14%2Fgemini-draft-follow-up-message.jpg" alt="Gemini drafting a follow-up message for the customer" width="800" height="476"&gt;

&lt;p&gt;It doesn't try to scrape the page. &lt;/p&gt;

&lt;p&gt;It calls the appropriate WebMCP tool, reads the signal state, and gives an accurate answer based on our actual business logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why WebMCP Tools Matter for Angular Apps
&lt;/h2&gt;

&lt;p&gt;By exposing app-defined capabilities, we turn the browser into a collaborative environment where the AI understands the context of our application.&lt;/p&gt;

&lt;p&gt;The AI response changes as the underlying signal state changes. &lt;/p&gt;

&lt;p&gt;This is the real value of WebMCP in Angular: safe, explicit, and reactive AI capabilities backed by your existing services and logic.&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;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-webmcp-tools" 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/3b0ae5fef0328477ee0f5d51980217e7c583a606" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;WebMCP unofficial draft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chromewebstore.google.com/detail/WebMCP%20-%20Model%20Context%20Tool%20Inspector/gbpdfapgefenggkahomfgkhfehlcenpd" rel="noopener noreferrer"&gt;WebMCP / Model Context Tool Inspector&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>ai</category>
      <category>javascript</category>
    </item>
    <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>
  </channel>
</rss>
