<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Angular: From Zero to Expert]]></title><description><![CDATA[Your journey to Angular mastery starts here]]></description><link>https://www.codigotipado.com</link><image><url>https://substackcdn.com/image/fetch/$s_!AiG8!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8721161d-0d9e-4e1f-b7ac-fa66332ba0f9_1280x1280.png</url><title>Angular: From Zero to Expert</title><link>https://www.codigotipado.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 29 Oct 2026 05:06:09 GMT</lastBuildDate><atom:link href="https://www.codigotipado.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Amos Isaila]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[amosisaila@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[amosisaila@substack.com]]></itunes:email><itunes:name><![CDATA[Amos Isaila]]></itunes:name></itunes:owner><itunes:author><![CDATA[Amos Isaila]]></itunes:author><googleplay:owner><![CDATA[amosisaila@substack.com]]></googleplay:owner><googleplay:email><![CDATA[amosisaila@substack.com]]></googleplay:email><googleplay:author><![CDATA[Amos Isaila]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[NgRx 22: three changes that make SignalStore easier to use]]></title><description><![CDATA[A simple look at Resource Extensions, Dynamic Deep Signals, and reusable SignalStore feature types.]]></description><link>https://www.codigotipado.com/p/ngrx-22-three-changes-that-make-signalstore</link><guid isPermaLink="false">https://www.codigotipado.com/p/ngrx-22-three-changes-that-make-signalstore</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Mon, 26 Oct 2026 05:38:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1rqO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1rqO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1rqO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1rqO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1rqO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1rqO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1rqO!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:213007,&quot;alt&quot;:&quot;NgRX v22: 3 changes explained on SignalStore&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/222131639?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="NgRX v22: 3 changes explained on SignalStore" title="NgRX v22: 3 changes explained on SignalStore" srcset="https://substackcdn.com/image/fetch/$s_!1rqO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1rqO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1rqO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1rqO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef199f84-9536-4862-81b8-64b69b90f665_1920x1080.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>NgRx 22 makes a few common SignalStore tasks easier. You can keep old data on screen while new data loads, work with values such as <code>Movie | null</code>, and reuse the type of one store feature inside another.</p>
      <p>
          <a href="https://www.codigotipado.com/p/ngrx-22-three-changes-that-make-signalstore">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular sanitises by default. Here is where that sentence runs out.]]></title><description><![CDATA[Many of the most interesting vulnerabilities happened in places the phrase &#8220;Angular escapes untrusted values&#8221; does not reach.]]></description><link>https://www.codigotipado.com/p/angular-sanitises-by-default-here</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-sanitises-by-default-here</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Sun, 04 Oct 2026 20:50:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Eutw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Eutw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Eutw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Eutw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Eutw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Eutw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Eutw!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:125032,&quot;alt&quot;:&quot;Angular sanitises by default. Here is where that sentence runs out.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/218735507?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular sanitises by default. Here is where that sentence runs out." title="Angular sanitises by default. Here is where that sentence runs out." srcset="https://substackcdn.com/image/fetch/$s_!Eutw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Eutw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Eutw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Eutw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd780239b-bab1-4144-a611-e3c57daf7c02_1920x1080.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is a guided tour of representative vulnerability classes, not a claim to contain every vulnerability Angular could ever have. For each one I want to be specific about three things: what the mechanism actually is, what has to be true in your app for it to bite you, and what you do about it. Every version number and API name below was checked against the installed type definitions of <code>@angular/core@22.1.4</code>, <code>@angular/common@22.1.4</code>, <code>@angular/platform-browser@22.1.4</code> and <code>@angular/build@22.1.6</code>, or against the primary advisory. The official advisory list was rechecked on 16 September 2026. Where I&#8217;m reading someone else&#8217;s incident report rather than code, I&#8217;ll say so.</p>
      <p>
          <a href="https://www.codigotipado.com/p/angular-sanitises-by-default-here">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Hour That Doesn’t Exist]]></title><description><![CDATA[A field guide to dates, timezones, and the Angular APIs that quietly lie to you]]></description><link>https://www.codigotipado.com/p/the-hour-that-doesnt-exist</link><guid isPermaLink="false">https://www.codigotipado.com/p/the-hour-that-doesnt-exist</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Wed, 30 Sep 2026 03:28:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!OqHP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OqHP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OqHP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!OqHP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!OqHP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!OqHP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OqHP!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:261648,&quot;alt&quot;:&quot;A field guide to dates, timezones, and the Angular APIs that quietly lie to you&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/216059355?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="A field guide to dates, timezones, and the Angular APIs that quietly lie to you" title="A field guide to dates, timezones, and the Angular APIs that quietly lie to you" srcset="https://substackcdn.com/image/fetch/$s_!OqHP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!OqHP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!OqHP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!OqHP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f5ecab6-21be-4164-a2cc-0cdc0601bedf_1920x1080.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You have a bug. The cron expression was fine, the container was healthy, the logs were clean. There was simply no 2:30 that night. The clocks went from 01:59:59 straight to 03:00:00, and the thirty minutes past two never happened.</p>
      <p>
          <a href="https://www.codigotipado.com/p/the-hour-that-doesnt-exist">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular 22.2: What's new]]></title><description><![CDATA[Smarter forms, safer templates, and reactive routing.]]></description><link>https://www.codigotipado.com/p/angular-222-whats-new</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-222-whats-new</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Thu, 24 Sep 2026 21:30:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8o36!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8o36!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8o36!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!8o36!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!8o36!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!8o36!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8o36!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:421318,&quot;alt&quot;:&quot;Angular 22.2: What's new&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/211235622?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 22.2: What's new" title="Angular 22.2: What's new" srcset="https://substackcdn.com/image/fetch/$s_!8o36!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!8o36!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!8o36!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!8o36!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1bba489-baf9-4c25-a4bf-6450a6ecf528_1920x1080.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h1>&#127959;&#65039; Framework</h1><h2>Templates &amp; Compiler</h2><h3>a) Templates can access private component members &#8212; <a href="https://github.com/angular/angular/pull/70188">PR</a></h3><p>Angular templates can now read and call TypeScript <code>private</code> members declared directly on their component.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;0704672b-9a07-423f-a463-0a8cdddee0ae&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import {
  ChangeDetectionStrategy,
  Component,
} from '@angular/core';

@Component({
  selector: 'app-status',
  template: `&lt;p&gt;{{ _status }}&lt;/p&gt;`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StatusComponent {
  private readonly _status = 'Ready';
}</code></pre></div><p>Previously, the template type checker reported an error for <code>_status</code>. Angular 22.2 allows this direct access, which is helpful for members that exist only to support the component template.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>This applies to TypeScript <code>private</code> members of the component itself. It does not make a nested object&#8217;s private properties accessible, and it does not apply to JavaScript <code>#private</code> fields.</p><h3>b) Catch misspelled output bindings &#8212; <a href="https://github.com/angular/angular/pull/70527">PR</a></h3><p>The new <code>strictUnclaimedEventNames</code> option catches <code>camelCase</code> output names that do not belong to the component and are not native DOM events:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;8c2d6463-173d-4908-9541-4222691311c4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;!-- Typo: itemSelecetd instead of itemSelected &#10060; --&gt;
&lt;product-list (itemSelecetd)="selectProduct($event)" /&gt;</code></pre></div><p>Enable it explicitly in <code>tsconfig.json</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;180e023d-b05a-4cbe-805f-df908622d212&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "angularCompilerOptions": {
    "strictUnclaimedEventNames": true
  }
}</code></pre></div><p>It is disabled by default and intentionally ignores lowercase or dash-separated custom events to reduce false positives.</p><h2>Core &amp; Reactivity</h2><h3>a) Catch errors from dynamically created views &#8212; <a href="https://github.com/angular/angular/pull/70463">PR</a></h3><p>Angular can now catch rendering and lifecycle errors from a component created with <code>ViewContainerRef</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;33933fc2-4efb-412e-8c6b-ba50187e30ca&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">private readonly _container = inject(ViewContainerRef);

showReport(): void {
  this._container.createComponent(ReportComponent, {
    onError: (error: Error): void =&gt; console.error('Report failed', error),
  });
}</code></pre></div><p>Previously, these errors reached the global <code>ErrorHandler</code>. Now a dynamically created view can handle its own failure. The new error-boundary APIs are available as Developer Preview.</p><h3>b) Read an element&#8217;s <code>Injector</code> from a query &#8212; <a href="https://github.com/angular/angular/pull/70561">PR</a></h3><p><code>viewChild()</code> and <code>contentChild()</code> can now return the injector attached to the matched element.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;a56116a9-8386-4270-8253-3dcd8615b88f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">@Component({
  imports: [ThirdPartyWidgetComponent],
  template: `&lt;third-party-widget #widget /&gt;`,
})
export class WidgetHostComponent {
  readonly widgetInjector = viewChild('widget', { read: Injector });
}</code></pre></div><p>This lets wrapper components access providers exactly as the child element sees them. It removes the need to create a helper directive just to capture that injector. This is a public API.</p><p></p><h2>Forms</h2><h3>a) Permanently hidden fields in Signal Forms &#8212; <a href="https://github.com/angular/angular/pull/69820">PR</a></h3><p>Signal Forms can now mark a field as permanently hidden. Call <code>hidden()</code> with only the field path, without a condition or configuration object.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;6b396245-1b14-416c-91bf-8829010ff127&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { signal } from '@angular/core';
import { form, hidden } from '@angular/forms/signals';

const profile = signal({
  name: '',
  internalId: '',
});

const profileForm = form(profile, (path) =&gt; {
  hidden(path.internalId);
});</code></pre></div><p><code>internalId</code> stays hidden even when its value changes. Angular also excludes the field from active validation while it is hidden.</p><p>This is useful for fields that belong to the model but should never appear in the current form, such as internal identifiers or values managed by the application.</p><h3>b) Signal Forms explain their behavior to AI agents &#8212; <a href="https://github.com/angular/angular/pull/70496">PR</a></h3><p>An experimental WebMCP Signal Form can be filled and submitted by a browser AI agent. Angular now tells the agent two things about that form automatically:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;eb0ec17c-908a-4d02-9631-e472222f26db&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">{
  readOnlyHint: false,
  untrustedContentHint: false,
}</code></pre></div><ul><li><p><code>readOnlyHint: false</code>: submitting the form changes the page or application state.</p></li><li><p><code>untrustedContentHint: false</code>: Angular currently considers the form response trusted.</p></li></ul><p>There is nothing new to configure. This is extra safety information that Angular adds behind the scenes. WebMCP support is still experimental.</p><h2>Router</h2><h3>a) Throw <code>RedirectCommand</code> from guards and resolvers &#8212; <a href="https://github.com/angular/angular/pull/70141">PR</a></h3><p>Guards and resolvers can now throw a <code>RedirectCommand</code> to stop the current navigation and redirect immediately.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;47a20aa8-bfe7-423f-a9d5-db42d628f5d5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { inject } from '@angular/core';
import {
  CanActivateFn,
  RedirectCommand,
  Router,
} from '@angular/router';

export const authGuard: CanActivateFn = () =&gt; {
  const router = inject(Router);

  throw new RedirectCommand(router.parseUrl('/login'));
};</code></pre></div><p>Previously, a nested helper had to return the redirect through every function in the call chain. Now it can throw the command directly, keeping the guard or resolver return type focused on its normal result.</p><h3>b) Public <code>containsTree</code> API for URL matching &#8212; <a href="https://github.com/angular/angular/pull/70148">PR</a></h3><p>The Router now exports <code>containsTree</code>, allowing applications and libraries to compare two <code>UrlTree</code> objects directly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;26814899-6454-4042-8ffd-00f792058d83&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { inject, Injectable } from '@angular/core';
import { containsTree, Router } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class UrlMatcher {
  private readonly _router = inject(Router);

  isInside(url: string, parent: string): boolean {
    return containsTree(
      this._router.parseUrl(url),
      this._router.parseUrl(parent),
    );
  }
}</code></pre></div><p>Calling <code>isInside('/products/42/details', '/products')</code> returns <code>true</code>.</p><p>By default, paths and query parameters use subset matching. You can pass options such as <code>{ paths: 'exact' }</code> when you need stricter behavior.</p><p>This is useful for custom navigation components, breadcrumbs and route-aware libraries that need URL matching without depending on private Router internals.</p><h3>c) &#9888;&#65039; Automatic route-injector cleanup is now stable &#8212; <a href="https://github.com/angular/angular/pull/70443">PR</a></h3><p>Detached routes can leave their injectors&#8212;and the services created inside them&#8212;alive. Enabling this feature lets Angular destroy an injector when its route is no longer active or saved for reuse.</p><p>The feature is now stable. Only its name changes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;afd10cdc-7e91-474c-81e2-5260317b7c07&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060;
provideRouter(routes, withExperimentalAutoCleanupInjectors());

// Angular 22.2 &#9989;
provideRouter(routes, withAutoCleanupInjectors());</code></pre></div><p>The old function still works, but it is deprecated. Custom <code>RouteReuseStrategy</code> implementations also get stable hooks for telling Angular which saved routes must keep their injectors.</p><h3>d) Load route data with Signals and Resources &#8212; <a href="https://github.com/angular/angular/pull/70437">PR</a></h3><p>Router Resources are a reactive alternative to resolvers. They can load data for a route, expose loading and error states, and reload without navigating again.</p><p>First, enable them:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;15b64d6b-8b5d-4c9c-af43-0bc375c7244b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">provideRouter(routes, withRouterResources());</code></pre></div><p>Then define the resource on the route:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;65d4aced-c9d1-499e-a493-6111dd2b2c21&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">{
  path: 'products/:id',
  component: ProductPageComponent,
  resources: (route) =&gt; {
    const products = inject(ProductService);

    return {
      product: resource({
        params: () =&gt; String(route.params()['id']),
        loader: ({ params: id }) =&gt; products.get(id),
      }),
    };
  },
}</code></pre></div><p>Resources from parent and child routes load in parallel, avoiding the sequential waits that nested resolvers can create. They are public in Angular 22.2, but remain in Developer Preview.</p><h2>Developer Tooling</h2><h3>a) Explicit workspace roots for Angular MCP &#8212; <a href="https://github.com/angular/angular-cli/pull/33746">PR</a></h3><p>The Angular MCP command now accepts <code>--root</code>. It defines which directories the MCP server may access and use for workspace discovery.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;0f0ad2fe-59d0-4b04-87c7-72e7cdb1b3f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">ng mcp --root ./frontend --root ./shared</code></pre></div><p>You can repeat the option for multi-root workspaces. This also lets an MCP host start Angular outside the project root while still passing the allowed directories explicitly.</p><h3>b) Test directives without creating a host component &#8212; <a href="https://github.com/angular/angular/pull/70453">PR</a></h3><p>Previously, testing a directive usually required creating a temporary host component. The new public <code>TestBed.createDirective()</code> API creates the directive and its HTML element directly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;bb4fb8df-4b7a-4b6a-bd26-f0a756bee3c7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { Directive } from '@angular/core';
import { TestBed } from '@angular/core/testing';

@Directive({
  selector: '[appHighlight]',
  host: { class: 'highlighted' },
})
class HighlightDirective {}

it('highlights its host element', (): void =&gt; {
  const fixture = TestBed.createDirective(HighlightDirective, {
    tagName: 'button',
  });

  fixture.detectChanges();

  expect(fixture.nativeElement.classList.contains('highlighted')).toBe(true);
});</code></pre></div><p>The fixture gives us the directive instance, its real host element and change detection. Tests become shorter because the artificial host component disappears.</p><h3>c) WebMCP tools can declare safety annotations &#8212; <a href="https://github.com/angular/angular/pull/70496">PR</a></h3><p>WebMCP lets a web application expose actions to browser AI agents. A tool can now describe its safety characteristics through two annotations:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;76464d2f-4e5f-4998-bba7-60348e288d5d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { provideExperimentalWebMcpTools } from '@angular/core';

export const webMcpProviders = provideExperimentalWebMcpTools([
  {
    name: 'readAccountStatus',
    description: 'Reads the current account status.',
    inputSchema: { type: 'object', properties: {} },
    annotations: {
      readOnlyHint: true,
      untrustedContentHint: false,
    },
    execute: () =&gt; ({
      content: [{ type: 'text', text: 'The account is active.' }],
    }),
  },
]);</code></pre></div><p>Here, <code>readOnlyHint: true</code> tells the agent that calling the tool changes nothing. <code>untrustedContentHint: false</code> says its response is controlled by the application rather than an external source.</p><p>The names are a bit technical, but the idea is simple: the AI agent gets more context before deciding whether and how to use a tool. These are hints, not security enforcement, and WebMCP remains experimental.</p><h2>Language Tooling</h2><h3>a) Editor support for <code>@boundary</code> blocks &#8212; <a href="https://github.com/angular/angular/pull/70463">PR</a></h3><p>The Angular Language Service now understands the new error-boundary syntax:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;057882ae-9c6b-404a-a85b-70bdaf66ace6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">@boundary {
  &lt;payment-widget /&gt;
} @error (let error) {
  &lt;p&gt;Payment failed: {{ error.message }}&lt;/p&gt;
}</code></pre></div><p>Editors can now highlight, fold and navigate these blocks correctly, as well as provide hover information. This does not add another application API; it adds editor support for the Developer Preview error-boundary feature.</p><h1>&#128736;&#65039; CLI &amp; Build Tooling</h1><h2>Build System</h2><h3>a) Angular Linker moves from Babel to OXC &#8212; <a href="https://github.com/angular/angular-cli/pull/33625">PR</a></h3><p>The Angular Linker in the build pipeline now uses <code>oxc-parser</code> and <code>magic-string</code> instead of <code>@babel/core</code> and the Angular Linker Babel plugin.</p><p>OXC provides the syntax tree and exact token positions, while <code>magic-string</code> updates partial Angular declarations directly in the original source. This removes Babel from this part of the build pipeline and reduces the amount of work needed to link partially compiled libraries.</p><h3>b) &#9888;&#65039; Separate browser and server build statistics &#8212; <a href="https://github.com/angular/angular-cli/pull/33870">PR</a></h3><p>Angular can now produce separate statistics for browser, server and initial bundles. This makes the output easier to load into bundle analyzers, especially for SSR applications where one combined file contains too much unrelated data.</p><p>Generate the files with the existing option:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;26b0c07e-4fd2-4d56-9c3e-7d4f1387ab88&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">ng build --stats-json</code></pre></div><p>The generated output has changed:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;220091a2-4070-48d9-af74-0f1acb7fdeec&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown">// Before &#10060;
dist/stats.json

// After &#9989;
dist/browser-stats.json
dist/browser-initial-stats.json
dist/server-stats.json          # SSR only
dist/server-initial-stats.json  # SSR only</code></pre></div><p>The <code>initial</code> files contain only the initial page bundles, while the full files include every corresponding browser or server output.</p><p>This is a breaking change for scripts and CI jobs that expect <code>stats.json</code>. Update those paths to the appropriate new file. For example, use <code>browser-initial-stats.json</code> when you only want to inspect the initial browser payload.</p><h1>&#9888;&#65039; Breaking Changes &amp; Deprecations</h1><p>Angular 22.2 does not declare any formal breaking changes. However, it includes one build-output change that can break existing pipelines and two deprecations.</p><ol><li><p><strong>Build statistics file names</strong> &#8212; <a href="https://github.com/angular/angular-cli/pull/33870">PR #33870</a><br>Enabling <code>statsJson</code> no longer creates <code>stats.json</code>. Browser builds now generate <code>browser-stats.json</code> and <code>browser-initial-stats.json</code>; SSR builds also generate their server equivalents. Update scripts and analyzers that expect the old filename.</p></li><li><p><strong>Deprecated experimental Router cleanup name</strong> &#8212; <a href="https://github.com/angular/angular/pull/70443">PR #70443</a><br>Replace <code>withExperimentalAutoCleanupInjectors()</code> with the stable <code>withAutoCleanupInjectors()</code>. The deprecated function still delegates to the stable implementation, so this migration does not need to happen all at once.</p></li><li><p><strong>Deprecated </strong><code>splitting</code><strong> option in the unit-test builder</strong> &#8212; <a href="https://github.com/angular/angular-cli/pull/34031">PR #34031</a><br>Remove <code>splitting</code> from the options of <code>@angular/build:unit-test</code>. It is no longer necessary with Vitest 5 and has no replacement.</p></li></ol><h2><strong>Thanks for reading so far &#128591;</strong></h2><p>I&#8217;d like to have your feedback, so please leave a <strong>comment</strong>, <strong>clap</strong> or <strong>follow</strong>. &#128079;</p><p>Spread the Angular love! &#128156;</p><p>If you liked it, <strong>share it</strong> among your community, tech bros and whoever you want! &#128640;&#128101;</p><p>Don&#8217;t forget to follow me and stay in the loop: &#128241;</p><ul><li><p><span>&#128279; </span><strong><a href="https://www.linkedin.com/in/amos-isaila-34ab78146/">LinkedIn</a></strong></p></li><li><p><span>&#128221; </span><strong><a href="https://medium.com/@amosisaila">Medium</a></strong></p></li><li><p><span>&#127909; </span><strong><a href="https://www.youtube.com/@codigotipado">YouTube</a></strong></p></li><li><p><span>&#128038; </span><strong><a href="https://x.com/amosisaila">X</a></strong></p></li></ul><p>Thanks for being part of this Angular adventure! &#128075;&#128513;</p>]]></content:encoded></item><item><title><![CDATA[Angular Content Projection Has a Real Design Problem]]></title><description><![CDATA[Content projection in simple words: Angular assigns projected nodes to slots once, when it creates the component.]]></description><link>https://www.codigotipado.com/p/angular-content-projection-has-a</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-content-projection-has-a</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Sun, 30 Aug 2026 22:26:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!YfmZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YfmZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YfmZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!YfmZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!YfmZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!YfmZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YfmZ!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:187904,&quot;alt&quot;:&quot;Angular Content Projection Has a Real Design Problem&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/213069489?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular Content Projection Has a Real Design Problem" title="Angular Content Projection Has a Real Design Problem" srcset="https://substackcdn.com/image/fetch/$s_!YfmZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!YfmZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!YfmZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!YfmZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe98e5916-6363-4ad1-9d68-0903af50718e_1920x1080.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Look how simple is content projection in Angular:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;83759368-bf0f-43aa-8a6c-996379cc9c6d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;app-card&gt;
  &lt;card-title&gt;Signals and forms&lt;/card-title&gt;
  &lt;p&gt;The body of the card.&lt;/p&gt;
&lt;/app-card&gt;</code></pre></div><p>The card decides where each node goes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;4c45cf3b-bb07-4088-8e0c-5154f7a7b8df&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;header&gt;
  &lt;ng-content select="card-title" /&gt;
&lt;/header&gt;

&lt;section&gt;
  &lt;ng-content /&gt;
&lt;/section&gt;</code></pre></div><p>This works well until the projected content becomes conditional, deferred, repeated, or dynamically created. Then we run into fallback content that never appears, <code>ngProjectAs</code> workarounds, awkward <code>TemplateRef</code> APIs, and <code>projectableNodes</code> arrays that require knowledge of a component&#8217;s internal slot order.</p><p>The Angular team is now collecting these cases in <a href="https://github.com/angular/angular/issues/64504">Canonical: Content Projection &#8212; issue #64504</a>. The important point is that Angular does not see them as separate bugs. They are consequences of the same underlying model.</p>
      <p>
          <a href="https://www.codigotipado.com/p/angular-content-projection-has-a">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[What signals actually did to Angular lifecycle hooks]]></title><description><![CDATA[You cannot replace every Angular lifecycle hook with a signal, and that is the interesting part.]]></description><link>https://www.codigotipado.com/p/what-signals-actually-did-to-angular</link><guid isPermaLink="false">https://www.codigotipado.com/p/what-signals-actually-did-to-angular</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Tue, 25 Aug 2026 22:44:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!-1Zj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-1Zj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-1Zj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-1Zj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-1Zj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-1Zj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-1Zj!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:214910,&quot;alt&quot;:&quot;What signals actually did to Angular lifecycle hooks&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/211955737?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="What signals actually did to Angular lifecycle hooks" title="What signals actually did to Angular lifecycle hooks" srcset="https://substackcdn.com/image/fetch/$s_!-1Zj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-1Zj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-1Zj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-1Zj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F607b5ad8-31e2-4304-a24e-baee56e384c3_1920x1080.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Two of the eight hooks aren&#8217;t replaced by signals at all.</p><p>Let&#8217;s start with what&#8217;s actually in the framework. I checked <code>@angular/core@22.1.2</code> and all eight lifecycle interfaces are still exported. Not one of them is marked deprecated. They all live in a single file, <a href="https://github.com/angular/angular/blob/v22.1.2/packages/core/src/change_detection/lifecycle_hooks.ts">packages/core/src/change_detection/lifecycle_hooks.ts</a>, pinned here to the <code>v22.1.2</code> tag:</p><ul><li><p><code>OnChanges</code></p></li><li><p><code>OnInit</code></p></li><li><p><code>DoCheck</code></p></li><li><p><code>OnDestroy</code></p></li><li><p><code>AfterContentInit</code></p></li><li><p><code>AfterContentChecked</code></p></li><li><p><code>AfterViewInit</code></p></li><li><p><code>AfterViewChecked</code></p></li></ul><p>I built a small app where each hook&#8217;s old and new version sit side by side, and I&#8217;ll point at it as we go. I also managed to write a genuinely bad bug in that app, which turned out to be the best part of the exercise.</p>
      <p>
          <a href="https://www.codigotipado.com/p/what-signals-actually-did-to-angular">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular 22.1: What's new]]></title><description><![CDATA[Angular 22.1 improves reactivity, styling, SSR and developer tooling while delivering faster builds with OXC.]]></description><link>https://www.codigotipado.com/p/angular-221-whats-new</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-221-whats-new</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Thu, 30 Jul 2026 00:22:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!sTTk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sTTk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sTTk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg 424w, https://substackcdn.com/image/fetch/$s_!sTTk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg 848w, https://substackcdn.com/image/fetch/$s_!sTTk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!sTTk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sTTk!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg" width="1200" height="670.054945054945" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:813,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:350139,&quot;alt&quot;:&quot;Angular 22.1 What's new&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/203165042?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 22.1 What's new" title="Angular 22.1 What's new" srcset="https://substackcdn.com/image/fetch/$s_!sTTk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg 424w, https://substackcdn.com/image/fetch/$s_!sTTk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg 848w, https://substackcdn.com/image/fetch/$s_!sTTk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!sTTk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff98f507c-68ed-4ca2-b5fd-c0dd8c75d85a_1920x1072.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h1>&#127959;&#65039; Framework</h1><h2>Core &amp; Reactivity</h2><h3>a) Custom <code>set</code> option for <code>linkedSignal</code> - <a href="https://github.com/angular/angular/pull/68708">PR</a></h3><p><code>linkedSignal</code> now accepts a custom <code>set</code> callback. It lets you redirect writes to the original source signal instead of replacing the linked signal&#8217;s internal value.</p><p>Consider a temperature value stored in Celsius but displayed and edited in Fahrenheit:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;38551b37-95ad-4421-bf79-d91bda46ca9e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { linkedSignal, signal } from '@angular/core';

const tempC = signal(0);

const tempF = linkedSignal(
  () =&gt; (tempC() * 9) / 5 + 32,
  {
    set: (valueF: number) =&gt; {
      tempC.set(((valueF - 32) * 5) / 9);
    },
  },
);

console.log(tempF()); // 32

tempF.set(212);

console.log(tempC()); // 100
console.log(tempF()); // 212</code></pre></div><p>Calling <code>tempF.set(212)</code> redirects the update to <code>tempC</code>. Since <code>tempF</code> depends on <code>tempC</code>, it recalculates automatically and the reactive graph remains consistent.</p><p>The callback also receives a second argument called <code>rawSet</code>. You can use it when you need to update the linked signal directly and preserve its original write behavior.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;b69af529-89ba-48ce-8893-7154a311e277&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">const value = linkedSignal(source, {
  set: (
    newValue: number,
    rawSet: (value: number) =&gt; void,
  ): void =&gt; {
    if (shouldUpdateSource(newValue)) {
      source.set(newValue);
    } else {
      rawSet(newValue);
    }
  },
});</code></pre></div><p>This is useful for editable derived values, form-like state and properties extracted from larger parent signals.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/p/angular-221-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Angular: From Zero to Expert! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/p/angular-221-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.codigotipado.com/p/angular-221-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h2>Templates &amp; Compiler</h2><h3>a) CSS Variable Namespacing - <a href="https://github.com/angular/angular/pull/68846">PR</a></h3><p>Running multiple Angular applications on the same page can cause CSS variables to leak between them. One application may set <code>--primary-color</code>, and another embedded application can accidentally inherit it.</p><p>Angular 22.1 introduces CSS variable namespacing for component styles:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;3377867f-cf17-4e51-acd0-348e4f17e416&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import {
  APP_ID,
  ApplicationConfig,
} from '@angular/core';
import { provideCssVarNamespacing } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_ID,
      useValue: 'storefront',
    },
    provideCssVarNamespacing('storefront_'),
  ],
};</code></pre></div><p>You can continue writing regular CSS variables:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;css&quot;,&quot;nodeId&quot;:&quot;6621e12a-edc7-426c-a1c7-d86815f704fa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-css">:host {
  --primary-color: blue;
  color: var(--primary-color);
}</code></pre></div><p>Angular transforms them using the configured namespace:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;css&quot;,&quot;nodeId&quot;:&quot;56d0c01e-1953-4a71-bd63-14292740e4f9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-css">:host {
  --my-app_primary-color: blue;
  color: var(--my-app_primary-color);
}</code></pre></div><p>Only styles declared through <code>styles</code> or <code>styleUrls</code> in Angular components are transformed. Global stylesheets remain unchanged.</p><p>Variables beginning with <code>--global--</code> are deliberately excluded:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;css&quot;,&quot;nodeId&quot;:&quot;a99f00a5-c624-4af5-b979-8f469f424109&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-css">:host {
  --global--shared-spacing: 16px;
}</code></pre></div><p><span>Template bindings such as </span><code>[style.--highlight]</code><span> are also namespaced. Code that accesses variables programmatically should use </span><code>CssVarNamespacer</code><span>, especially in libraries where the consuming application may enable namespacing.</span></p><h3>b) Smaller generated output for foreign components&#8202;&#8212;&#8202;<a href="https://github.com/angular/angular/pull/69674">PR</a></h3><p>The compiler no longer emits a redundant tag name when a foreign component is the root of an <code>@if</code>, <code>@for</code> or <code>@switch</code> block.</p><p>This is a small internal performance improvement that reduces generated output without requiring application changes.</p><h2>HTTP</h2><h3>a) More control over the HTTP transfer cache - <a href="https://github.com/angular/angular/pull/69628">PR</a></h3><p>Angular&#8217;s HTTP transfer cache stores server responses during SSR so the browser doesn&#8217;t immediately request the same data again during hydration.</p><p>Angular 22.1 adds two options:</p><ul><li><p><code>includeRequestsWithCredentials</code></p></li><li><p><code>includeNonCacheableRequests</code></p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;60f5feab-1b15-46ea-9bea-d1d98d44fb50&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { ApplicationConfig } from '@angular/core';
import {
  provideClientHydration,
  withHttpTransferCacheOptions,
} from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(
      withHttpTransferCacheOptions({
        includeRequestsWithCredentials: true,
        includeNonCacheableRequests: true,
      }),
    ),
  ],
};</code></pre></div><p><code>includeRequestsWithCredentials</code> allows requests using <code>withCredentials</code> or Fetch credential modes to enter the transfer cache.</p><p><code>includeNonCacheableRequests</code> permits requests and responses normally excluded by <code>Cache-Control</code>, <code>Set-Cookie</code> or Fetch cache options.</p><p>Both options default to <code>false</code>. Enable them only when the response is safe to serialize into the server-rendered page&#8212;particularly when SSR output might be cached or shared.</p><h3>b) &#9888;&#65039; JSONP support is deprecated&#8202;&#8212;&#8202;<a href="https://github.com/angular/angular/pull/69116">PR</a></h3><p>Angular has deprecated JSONP because it executes arbitrary scripts in the global context. This can bypass modern Content Security Policies and introduce cross-site scripting risks.</p><p>The deprecation covers:</p><ul><li><p><code>HttpClient.jsonp()</code></p></li><li><p><code>HttpClientJsonpModule</code></p></li><li><p><code>JsonpClientBackend</code></p></li><li><p><code>JsonpInterceptor</code></p></li><li><p><code>jsonpInterceptorFn</code></p></li><li><p><code>jsonpCallbackContext</code></p></li><li><p><code>withJsonpSupport()</code></p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;46d2b77e-1c36-46a2-815d-5649db6ace30&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060;
this._http.jsonp&lt;DataResponse&gt;(
  &#8216;https://api.example.com/data&#8217;,
  &#8216;callback&#8217;,
);

// After &#9989;
this._http.get&lt;DataResponse&gt;(
  &#8216;https://api.example.com/data&#8217;,
);</code></pre></div><p>Remove <code>withJsonpSupport()</code> from your HTTP providers as part of the migration.</p><p>If an external API only supports JSONP, use a backend proxy or move to an endpoint supporting CORS. Angular intends to remove JSONP in a future version.</p><h2>Router</h2><h3>a) Nullable inputs for <code>RouterLinkActive</code> - <a href="https://github.com/angular/angular/pull/68120">PR</a></h3><p><code>RouterLinkActive</code> now accepts <code>null</code> and <code>undefined</code> for both <code>routerLinkActive</code> and <code>routerLinkActiveOptions</code>.</p><p>Reusable link components previously needed duplicated <code>@if</code> branches when active styling was optional. The directive can now remain in one template:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;2fdbf1bd-3a2e-4e90-a612-0db7e1abd6e6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;a
  [routerLink]=&#8221;href()&#8221;
  [routerLinkActive]=&#8221;activeClass()&#8221;
  [routerLinkActiveOptions]=&#8221;activeOptions()&#8221;
&gt;
  &lt;ng-content /&gt;
&lt;/a&gt;</code></pre></div><p>The behavior depends on the input:</p><ul><li><p><code>routerLinkActive: null | undefined</code> applies no active classes.</p></li><li><p><code>routerLinkActiveOptions: undefined</code> uses the default subset match.</p></li><li><p><code>routerLinkActiveOptions: null</code> disables active matching.</p></li></ul><p>That means fewer conditional branches and fewer unnecessary comment nodes in reusable navigation components.</p><h2>Language Tooling</h2><h3>a) Compile non-exported classes if standalone - <a href="https://github.com/angular/angular/pull/68454">PR</a></h3><p>The Angular Language Service now compiles standalone classes that aren&#8217;t exported from their file. Previously, if you had a component that wasn&#8217;t exported (common in single-file route components or test files), the language service would skip it &#8212; meaning no autocompletion, no type-checking, no diagnostics in your template.</p><h3>b) Typecheck templates requiring inline typecheck blocks - <a href="https://github.com/angular/angular/pull/68454">PR</a></h3><p>Part of the same PR &#8212; the language service can now type-check templates that would require inline type-check blocks. Previously, certain template patterns that the compiler couldn&#8217;t handle with external type-check files were simply skipped by the language service. Now they get full diagnostics.</p><h2>Developer Tooling</h2><h3>a) Deep linking from Chrome&#8217;s Performance panel - <a href="https://github.com/angular/angular/pull/68201">PR</a></h3><p>Angular component events recorded in Chrome&#8217;s Performance panel can now link directly to the corresponding component inside Angular DevTools.</p><p>The recorded event contains a URL using this format:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;c4e1d783-5be2-4dd1-9d54-2eb48c9c1e6a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown">angular-devtools://component/{instanceId}</code></pre></div><p>Selecting it opens that specific component instance in Angular DevTools, making it easier to move from a performance problem to the component responsible for it.</p><p>The integration currently depends on Chrome&#8217;s DevTools extensibility support and may require enabling:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;eed0e782-7794-4766-8be4-70bff2e6b04f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown">chrome://flags/#enable-devtools-deep-link-via-extensibility-api</code></pre></div><h1>&#128736;&#65039; CLI &amp; Build Tooling</h1><h3>a) Angular MCP server enabled by default in new workspaces - <a href="https://github.com/angular/angular-cli/pull/32973">PR</a></h3><p>The <code>ai-config</code> schematic now includes Angular MCP server configuration.</p><p>New workspaces created with AI configuration can connect supported AI tools to Angular&#8217;s development capabilities with less manual setup.</p><h3>b) &#9888;&#65039; Deprecation of <code>stringToFileBuffer</code> and <code>fileBufferToString</code> in <code>@angular-devkit/core</code></h3><p><code>stringToFileBuffer</code> and <code>fileBufferToString</code> from <code>@angular-devkit/core</code> are now deprecated. The replacement is the standard Web APIs: <code>TextEncoder</code> and <code>TextDecoder</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;10c2282e-d718-42c9-9fb5-ac7709ebac1e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060;
import { stringToFileBuffer, fileBufferToString } from '@angular-devkit/core';

const buffer = stringToFileBuffer('hello');
const str = fileBufferToString(buffer);
</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;b7eb8558-3609-4e6f-a427-52a1158fa494&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// After &#9989;
const encoder = new TextEncoder();
const decoder = new TextDecoder();

const buffer = encoder.encode('hello');
const str = decoder.decode(buffer);</code></pre></div><h3>c) Share Build Cache Across Git Worktrees - <a href="https://github.com/angular/angular-cli/pull/33519">PR</a></h3><p>Angular now shares its persistent build cache across Git worktrees belonging to the same repository.</p><p>Instead of creating a separate <code>.angular/cache</code> directory in every worktree, Angular resolves the cache from the main repository:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;21472e4e-fc58-4bc8-90bd-e87bb3554c5d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown">main-repository/
&#9500;&#9472;&#9472; .angular/cache/
&#9500;&#9472;&#9472; worktree-feature-a/
&#9492;&#9472;&#9472; worktree-feature-b/</code></pre></div><p>Regular repositories and Git submodules retain their existing behavior. If worktree detection fails, Angular safely falls back to the workspace-relative cache.</p><p>This reduces duplicated cache files and avoids rebuilding the same artifacts across parallel branches.</p><h3>d) Chunk optimization for server builds &#8212; <a href="https://github.com/angular/angular-cli/pull/33602">PR</a></h3><p>Angular can now optimize browser chunks even when an application uses SSR or has a server entry point.</p><p>For those unfamiliar with OXC: it&#8217;s a collection of high-performance JavaScript and TypeScript tools written in Rust. Angular uses its fast parser to reduce build times, with no extra configuration required.</p><h3>e) Faster production optimizations with OXC &#8212; <a href="https://github.com/angular/angular-cli/pull/33568">PR</a></h3><p>Angular moved several internal production optimizations from Babel to the faster OXC parser.</p><h3>f) Much faster localized builds &#8212; <a href="https://github.com/angular/angular-cli/pull/33601">PR</a></h3><p>Angular&#8217;s localization worker now uses OXC and magic-string instead of Babel.</p><h1>&#128230; Migrations &amp; Schematics</h1><h3>a) <code>@Injectable</code> to <code>@Service</code> migration schematic - <a href="https://github.com/angular/angular/pull/69455">PR</a></h3><p>Angular now ships an automated migration schematic that converts eligible <code>@Injectable</code> classes to the new <code>@Service</code> decorator. This is part of the broader push toward more semantic decorators &#8212; <code>@Service</code> communicates intent more clearly than <code>@Injectable</code>, which was always a bit of a misnomer (it describes <em>how</em> the class participates in DI, not <em>what</em> it is).</p><p>Run it with:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;01600a96-6cef-4db7-a737-f94c030380eb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">ng generate @angular/core:service</code></pre></div><p>Or target a specific directory:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;9e7620b9-1a94-465c-b0df-a723948f589e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">ng generate @angular/core:service --path src/app/features</code></pre></div><p><strong>What it does:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;35d8bb83-c7dc-44d0-beb1-7a8b1daa36c0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060;
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UserService {}</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;9562cf38-4f26-4dab-957a-22e274cfb244&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// After &#9989;
import { Service } from '@angular/core';

@Service()
export class UserService {}</code></pre></div><p>The schematic handles the import swap automatically &#8212; replacing <code>Injectable</code> with <code>Service</code> in the import statement and updating the decorator call.</p><p><strong>Limitations &#8212; the schematic will skip classes that:</strong></p><ul><li><p>Use constructor-based dependency injection (since <code>@Service</code> is designed for the <code>inject()</code> pattern)</p></li><li><p>Pass any options into <code>@Injectable</code> aside from <code>providedIn</code></p></li><li><p>Pass anything aside from <code>'root'</code> into <code>providedIn</code></p></li></ul><p>This is conservative by design. The schematic won&#8217;t touch classes using <code>useFactory</code>, <code>useClass</code>, <code>useValue</code>, or <code>useExisting</code> providers, nor will it touch services scoped to specific modules or components. Those cases require manual review.</p><p>If you&#8217;re already following modern Angular patterns (using <code>inject()</code> instead of constructor injection, providing services at root), this migration should cover the vast majority of your services with zero manual intervention.</p><h1>&#9888;&#65039; Breaking Changes &amp; Deprecations</h1><h3>1. JSONP Support Deprecated (<code>@angular/common/http</code>)</h3><p><code>HttpClient.jsonp</code>, <code>HttpClientJsonpModule</code>, <code>withJsonpSupport()</code>, and all related JSONP classes/functions are deprecated due to XSS vulnerability risks. Use standard HTTP requests with CORS instead. Intent to remove in a future Angular version.</p><p><strong>PR:</strong> <a href="https://github.com/angular/angular/pull/69116">#69116</a></p><h3>2. <code>stringToFileBuffer</code> and <code>fileBufferToString</code> Deprecated (<code>@angular-devkit/core</code>)</h3><p>These utility functions are deprecated in favor of standard Web APIs (<code>TextEncoder</code> and <code>TextDecoder</code>). Internal usages have already been removed and replaced.</p><h2><strong>Thanks for reading so far &#128591;</strong></h2><p>I&#8217;d like to have your feedback, so please leave a <strong>comment</strong>, <strong>clap</strong> or <strong>follow</strong>. &#128079;</p><p>Spread the Angular love! &#128156;</p><p>If you liked it, <strong>share it</strong> among your community, tech bros and whoever you want! &#128640;&#128101;</p><p>Don&#8217;t forget to follow me and stay in the loop: &#128241;</p><ul><li><p><span>&#128279; </span><strong><a href="https://www.linkedin.com/in/amos-isaila-34ab78146/">LinkedIn</a></strong></p></li><li><p><span>&#128221; </span><strong><a href="https://medium.com/@amosisaila">Medium</a></strong></p></li><li><p><span>&#127909; </span><strong><a href="https://www.youtube.com/@codigotipado">YouTube</a></strong></p></li><li><p><span>&#128038; </span><strong><a href="https://x.com/amosisaila">X</a></strong></p></li></ul><p>Thanks for being part of this Angular adventure! &#128075;&#128513;</p>]]></content:encoded></item><item><title><![CDATA[Angular 22: What's new]]></title><description><![CDATA[Angular 22: Signal Forms are stable. OnPush is the new default. The future is here.]]></description><link>https://www.codigotipado.com/p/angular-22-whats-new</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-22-whats-new</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Wed, 03 Jun 2026 22:01:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ZXwQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZXwQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg" width="1200" height="670.054945054945" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:813,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:268520,&quot;alt&quot;:&quot;Angular 22&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/193433490?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 22" title="Angular 22" srcset="https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ZXwQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0705989-0940-4c9e-9013-93236625e1f0_1920x1072.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>&#127959;&#65039; Framework (<code>angular/angular</code>)</h2><h3>Core &amp; Reactivity</h3><h4>a) &#128293; Default change detection is now OnPush - <a href="https://github.com/angular/angular/commit/eae8f7e30b9f8bebdcdb535bd86260199c34274b">PR</a></h4><p><strong>Components without an explicit </strong><code>changeDetection</code><strong> property now default to </strong><code>OnPush</code> instead of the old <code>Default</code> (now renamed to <code>Eager</code>).</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;e9ca131f-2bf4-45df-8f81-49db0f92f885&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before Angular 22 &#8212; this was implicitly ChangeDetectionStrategy.Default
@Component({
  selector: &#8216;app-dashboard&#8217;,
  template: `&lt;h1&gt;{{ title }}&lt;/h1&gt;`,
})
export class DashboardComponent {
  title = &#8216;Dashboard&#8217;;
}

// Angular 22 &#8212; same code is now implicitly OnPush
// If you need the old behavior:
@Component({
  selector: &#8216;app-dashboard&#8217;,
  changeDetection: ChangeDetectionStrategy.Eager, // &#8592; new name for &#8220;Default&#8221;
  template: `&lt;h1&gt;{{ title }}&lt;/h1&gt;`,
})
export class DashboardComponent {
  title = &#8216;Dashboard&#8217;;
}</code></pre></div><h4>b) Signal-native debouncing with <code>debounced()</code> - <a href="https://github.com/angular/angular/commit/b918beda323eefef17bf1de03fde3d402a3d4af0">PR</a></h4><p>Angular added a <code>debounced()</code> function that takes a signal and a wait time, and returns a <strong>Resource</strong>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;d30672c6-af68-4d3b-bc88-62bf38619b0e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">protected readonly searchInput = signal(&#8217;&#8216;);

// Debounced &#8212; settles after 500ms of no typing
protected readonly debouncedSearch = debounced(() =&gt; this.searchInput(), 500);</code></pre></div><p>The resource gives you <code>.value()</code>, <code>.status()</code>, <code>.isLoading()</code>, and <code>.error()</code> &#8212; loading states come for free. I wrote a full deep-dive on this: check out my <strong><a href="https://www.codigotipado.com/p/angular-22-debounced-signal">Debounced Signals in Angular 22</a></strong><a href="https://www.codigotipado.com/p/angular-22-debounced-signal"> article</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h4>c) Nested leave animations scoped to component boundaries - <a href="https://github.com/angular/angular/commit/df659b8d0cf64eeed418c60bc16cae5630086401">PR</a></h4><p>Leave animations are no longer limited to just the element being removed. They now propagate to nested elements within the same component boundary. If you have a parent element with <code>:leave</code> and child elements with their own animations, the children will now animate out properly before the parent is removed.</p><p>This is a <strong>breaking change</strong> if you relied on the old behavior where nested leave animations were silently ignored.</p><h4>d) Special return statuses for resource params - <a href="https://github.com/angular/angular/commit/2206efa55fc1de160333d62680f8893c47525335">PR</a></h4><p>Resources now support special return statuses from their <code>params</code> function. Instead of always returning params (which triggers a load), you can now return <code>undefined</code> to put the resource in an <code>idle</code> state &#8212; meaning it won&#8217;t fetch until valid params are available.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;db8cb4d4-2156-46e8-a642-409e277cf4bf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">type UserSearchParams = { query: string };

@Component({
  selector: &#8216;app-user-search&#8217;,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    &lt;input (input)=&#8221;searchQuery.set($any($event.target).value)&#8221; placeholder=&#8221;Search users...&#8221; /&gt;

    @switch (users.status()) {
      @case (&#8217;idle&#8217;) { &lt;p&gt;Type something to search&lt;/p&gt; }
      @case (&#8217;loading&#8217;) { &lt;p&gt;Searching...&lt;/p&gt; }
      @case (&#8217;resolved&#8217;) {
        @for (user of users.value(); track user.id) {
          &lt;p&gt;{{ user.name }}&lt;/p&gt;
        }
      }
      @case (&#8217;error&#8217;) { &lt;p&gt;Something went wrong&lt;/p&gt; }
    }
  `,
})
export class UserSearchComponent {
  protected readonly searchQuery = signal(&#8217;&#8216;);

  protected readonly users = httpResource&lt;User[]&gt;(() =&gt; {
    const query = this.searchQuery();

    // Return undefined &#8594; resource stays idle, no request fired
    if (query.length &lt; 2) return undefined;

    return `/api/users?q=${encodeURIComponent(query)}`;
  });
}</code></pre></div><p>The <code>idle</code> status is new &#8212; before this, a resource would always be in <code>loading</code>, <code>resolved</code>, or <code>error</code>. Now you can distinguish between &#8220;hasn&#8217;t started yet&#8221; and &#8220;currently fetching.&#8221;</p><p><a href="https://www.codigotipado.com/p/angular-19-resource-api-for-async">Learn more about Angular resources here</a>.</p><h4>e) <code>TestBed.getLastFixture()</code> - <a href="https://github.com/angular/angular/commit/dc3131c639542ad6a463bff3da5ca84c6f8ecb6f">PR</a></h4><p>A new testing utility that returns the last created <code>ComponentFixture</code>. Originally introduced as <code>TestBed.getFixture()</code> in next.0, it was renamed to <code>TestBed.getLastFixture()</code> in next.4 for clarity.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;29daf4c0-2438-41fa-babc-f1e695be1c7b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">it(&#8217;should render the component&#8217;, () =&gt; {
  TestBed.configureTestingModule({
    imports: [MyComponent],
  });

  const fixture = TestBed.createComponent(MyComponent);
  
  // Later in the test, if you need the fixture again:
  const sameFixture = TestBed.getLastFixture();
  expect(sameFixture).toBe(fixture);
});</code></pre></div><h4>f) Customizable @defer idle behavior - <a href="https://github.com/angular/angular/commit/a5981b83a60577d9068d2429bcbed969edca581b">PR</a> &amp; <a href="https://github.com/angular/angular/commit/98eb24cea0498382cc7cf7d7b85cd9ead5ad99ad">PR</a></h4><p>Two related changes that give you more control over <code>@defer</code> blocks with idle triggers:</p><ol><li><p><code>IdleRequestOptions</code><strong> support</strong> &#8212; you can now customize the idle service behavior</p></li><li><p><strong>Optional timeout for idle triggers</strong> &#8212; set a maximum wait time for idle-triggered defer blocks</p></li></ol><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;36ae273a-e3c2-470e-a2ee-31e8f33e7c92&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// You can now configure idle behavior with timeout
@defer (on idle; timeout 5000) {
  &lt;app-heavy-widget /&gt;
}</code></pre></div><p>This is useful when you want a defer block to load during idle time but don&#8217;t want to wait forever if the browser stays busy.</p><h4>g) Exhaustive type checking for <code>@switch</code> - <a href="https://github.com/angular/angular/commit/8bc31a515ff6e8edda6ea5786a47ae5a788acd36">PR</a></h4><p>The <code>@switch</code> control flow now supports exhaustive type checking with other expressions. The compiler can verify that all possible values of a union type are handled:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;f648b381-4d9a-4a99-b374-58bcc7ca31c1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">type Status = &#8216;active&#8217; | &#8216;inactive&#8217; | &#8216;pending&#8217;;

@Component({
  template: `
    @switch (status()) {
      @case (&#8217;active&#8217;) { &lt;span&gt;Active&lt;/span&gt; }
      @case (&#8217;inactive&#8217;) { &lt;span&gt;Inactive&lt;/span&gt; }
      @case (&#8217;pending&#8217;) { &lt;span&gt;Pending&lt;/span&gt; }
    }
  `,
})
export class StatusComponent {
  status = input.required&lt;Status&gt;();
}</code></pre></div><h4>h) &#9888;&#65039; <code>createNgModuleRef</code> removed - <a href="https://github.com/angular/angular/commit/36936872c962b2073c8f44080684701068866691">PR</a></h4><p>The deprecated <code>createNgModuleRef</code> function has been removed. Use <code>createNgModule</code> instead:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;9784791e-05a0-43e4-bc25-bbdfd43f61b0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// No more createNgModuleRef
const moduleRef = createNgModule(MyModule, injector);</code></pre></div><h4>i) &#9888;&#65039; <code>ChangeDetectorRef.checkNoChanges</code> removed - <a href="https://github.com/angular/angular/commit/69fb1614eff6e40bb7dcca81f275ac32b9cbd28a">PR</a></h4><p><code>ChangeDetectorRef.checkNoChanges</code> has been removed from the public API. In tests, use <code>fixture.detectChanges()</code> instead.</p><h4>j) &#9888;&#65039; AnimationCallbackEvent.animationComplete signature changed - <a href="https://github.com/angular/angular/commit/b9b5c279b444ab2684fe911982930dc7c31ed43c">PR</a></h4><p>The <code>animationComplete</code> callback signature on <code>AnimationCallbackEvent</code> has been enhanced. If you&#8217;re using this API directly, check the new signature.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;5f212263-34af-4b44-b9b0-b60e5189f742&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// strictly typed as VoidFunction
type AnimationCallbackEvent = {
  target: Element;
  animationComplete: VoidFunction;
};</code></pre></div><h4>k) Migration schematic: <code>ChangeDetectionStrategy.Eager</code> - <a href="https://github.com/angular/angular/commit/cb4cb77053a817fe800af6395783720761e29ada">PR</a></h4><p>Since the default changed to OnPush, Angular provides a migration schematic that automatically adds <code>ChangeDetectionStrategy.Eager</code> to components that previously relied on the implicit <code>Default</code> strategy. Run it during <code>ng update</code>.</p><h4>l) De-duplicate host directives - <a href="https://github.com/angular/angular/commit/9c55fcb3e65ffcde32d7ac438ea40a69ffc2b3b6">PR</a></h4><p>If the same directive appears multiple times in a host directive chain (through inheritance or composition), it&#8217;s only applied once. Before this, duplicates could cause unexpected double-initialization or conflicting behavior.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;9533058a-6d09-4b04-b464-34f06f142db9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">@Directive({ host: { class: &#8216;tooltip&#8217; } })
export class TooltipDirective {}

// Both BaseCard and FancyCard declare TooltipDirective as a host directive
@Component({
  selector: &#8216;app-base-card&#8217;,
  hostDirectives: [TooltipDirective],
  template: `&lt;ng-content /&gt;`,
})
export class BaseCardComponent {}

@Component({
  selector: &#8216;app-fancy-card&#8217;,
  hostDirectives: [TooltipDirective], // duplicate &#8212; now de-duplicated automatically
  template: `&lt;ng-content /&gt;`,
})
export class FancyCardComponent extends BaseCardComponent {}</code></pre></div><h4>m) &#9888;&#65039; Drop support for TypeScript 5.9 - <a href="https://github.com/angular/angular/commit/8fe025f5149d7eb460e784a5a17bb467f85b9080">PR</a></h4><p>Angular 22 now requires <strong>TypeScript 6.0 or later</strong>. TypeScript 5.9 and older are no longer supported. Make sure to update your TypeScript dependency when upgrading.</p><h4>n) &#9888;&#65039; <code>ComponentFactoryResolver</code> &amp; <code>ComponentFactory</code> removed - <a href="https://github.com/angular/angular/commit/9d76ac82290e047f1481fb38bd95233e951a77de">PR</a></h4><p><code>ComponentFactoryResolver</code> and <code>ComponentFactory</code> have been completely removed from the API surface. These were deprecated long ago in favor of passing component classes directly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;30ae526f-a93d-4985-b3db-10ee7a9c6b92&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060; &#8212; using the factory pattern
const factory = this._resolver.resolveComponentFactory(MyComponent);
const componentRef = viewContainerRef.createComponent(factory);

// After &#9989; &#8212; pass the component class directly
const componentRef = viewContainerRef.createComponent(MyComponent);

// Or use the standalone function:
const componentRef = createComponent(MyComponent, { environmentInjector });</code></pre></div><p>If you&#8217;re still using <code>ComponentFactoryResolver</code> anywhere &#8212; in dynamic component loading, lazy modules, or testing utilities &#8212; this is the migration you need to make.</p><h4>o) Bootstrap via <code>ApplicationRef</code> with config - <a href="https://github.com/angular/angular/commit/a0aa8304cd78a58a990c3b648e41f6888b50b1b3">PR</a></h4><p><code>ApplicationRef.bootstrap()</code> now accepts a configuration object as its second argument. The type has been tightened &#8212; it no longer accepts <code>any</code>, so you need to pass a proper non-nullable element.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;0d5b8dd0-f857-4543-9d86-96cbfddefbfe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#8212; second arg was loosely typed as `any`
appRef.bootstrap(AppComponent, document.getElementById(&#8217;app&#8217;));

// Angular 22 &#8212; stricter typing, element must be non-nullable
const rootEl = document.getElementById(&#8217;app&#8217;);
if (rootEl) {
  appRef.bootstrap(AppComponent, rootEl);
}</code></pre></div><p>This is a <strong>breaking change</strong> if you were passing a potentially nullable element without checking first.</p><h4>p) Synchronous Values for Stream Resources - <a href="https://github.com/angular/angular/pull/67382">PR</a></h4><p>Stream resources (<code>resource()</code> with a <code>stream</code> loader) previously required the loader to return a <code>PromiseLike&lt;Signal&lt;ResourceStreamItem&lt;T&gt;&gt;&gt;</code>. That async requirement meant the resource always started in a <code>loading</code> state &#8212; even when the data was available immediately.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/p/angular-22-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Angular: From Zero to Expert! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/p/angular-22-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.codigotipado.com/p/angular-22-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><p>This was a real problem for SSR scenarios. When you&#8217;re trying to cache resource values in <code>TransferState</code>, the resource needs to set its value synchronously. Otherwise, it enters <code>loading</code> state on the client, which destroys the server-hydrated DOM before the value arrives.</p><p>The <code>ResourceStreamingLoader</code> type now accepts three return types:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;fe979ee6-7f9f-492b-b6f5-068070974d63&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// New type signature
type ResourceStreamingLoader&lt;T, R&gt; = (
  param: ResourceLoaderParams&lt;R&gt;
) =&gt; Signal&lt;ResourceStreamItem&lt;T&gt;&gt;       // &#8592; NEW: synchronous
   | PromiseLike&lt;Signal&lt;ResourceStreamItem&lt;T&gt;&gt;&gt;  // existing async
   | undefined;                                   // &#8592; NEW: skip loading</code></pre></div><p>What this enables:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;de3f5ecc-328f-4f27-b05c-0e7c1a9b4199&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">@Component({
  template: `
    @if (weather.value(); as data) {
      &lt;p&gt;{{ data.temperature }}&#176;C in {{ data.city }}&lt;/p&gt;
    }
  `
})
export class WeatherComponent {
  private _city = input.required&lt;string&gt;();
  private _cache = inject(TransferStateCache);

  weather = resource({
    params: this._city,
    stream: ({ params: city }) =&gt; {
      // Check cache first &#8212; returns synchronously if cached
      const cached = this._cache.get&lt;WeatherData&gt;(`weather-${city}`);
      if (cached) {
        // Resource is immediately 'resolved', no loading flicker
        return signal({ value: cached });
      }

      // Fall back to async fetch
      return this._fetchWeather(city);
    },
  });

  private async _fetchWeather(
    city: string
  ): Promise&lt;Signal&lt;ResourceStreamItem&lt;WeatherData&gt;&gt;&gt; {
    const response = await fetch(`/api/weather?city=${city}`);
    const data: WeatherData = await response.json();
    return signal({ value: data });
  }
}</code></pre></div><p>The key win: <code>rxResource</code> with synchronous observables (like <code>of(value)</code>) now resolves immediately after a tick instead of requiring <code>await appRef.whenStable()</code>. This makes SSR hydration seamless &#8212; the client picks up the cached value without any loading state flash.</p><h4>q) Support Bootstrapping Under Shadow Roots - <a href="https://github.com/angular/angular/pull/67947">PR</a></h4><p>Angular applications can now be bootstrapped inside a Shadow DOM root. Previously, <code>bootstrapApplication</code> assumed it was working with the document&#8217;s root &#8212; which broke when you tried to embed an Angular app inside a web component&#8217;s shadow root.</p><p>This is a big deal for micro-frontend architectures and web component integration scenarios:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;85610072-bfc2-4706-913f-90c1af19fe60&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Bootstrap Angular inside a shadow root
const shadowRoot = hostElement.attachShadow({ mode: 'open' });

const appDiv = document.createElement('div');
appDiv.id = 'angular-app';
shadowRoot.appendChild(appDiv);

bootstrapApplication(AppComponent, {
  ...appConfig,
  // Angular now correctly scopes to the shadow root
  // instead of assuming document-level access
});</code></pre></div><p>This means Angular can coexist with other frameworks on the same page without style or DOM conflicts, each living in its own shadow boundary. If you&#8217;re building micro-frontends or embedding Angular widgets in non-Angular pages, this removes a significant barrier.</p><h4>r) The New <code>@Service</code> Decorator - <a href="https://github.com/angular/angular/pull/68195">PR</a></h4><p>This is a big one. Angular now has a dedicated <code>@Service</code> decorator that&#8217;s purpose-built for the most common use case: defining a singleton service available across your entire app.</p><p><code>@Injectable</code> has been around since Angular 2, and it carries a lot of historical baggage. Most developers just want to create a service that&#8217;s <code>providedIn: 'root'</code>, uses <code>inject()</code> for dependencies, and doesn&#8217;t need the complex provider configurations (<code>useClass</code>, <code>useValue</code>, <code>useFactory</code>, etc.). Yet every time, you have to write <code>@Injectable({ providedIn: 'root' })</code> &#8212; boilerplate that adds noise without adding value.</p><p><code>@Service</code> fixes this by making the common case the default. Here&#8217;s what&#8217;s different:</p><ol><li><p><code>providedIn: 'root'</code><strong> by default.</strong> No configuration needed for the 90% case. If you want to provide the service yourself, set <code>autoProvided: false</code>.</p></li><li><p><strong>No constructor-based injection.</strong> Only the <code>inject()</code> function is supported. This aligns with where Angular has been heading for a while now.</p></li><li><p><strong>Simplified factory support.</strong> Instead of the complex <code>useClass</code>/<code>useValue</code>/<code>useExisting</code>/<code>useFactory</code> signature from <code>@Injectable</code>, <code>@Service</code> supports a single <code>factory</code> function.</p></li></ol><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;2ad070d6-259c-4b4e-b242-3cebbd28c285&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before: @Injectable with boilerplate &#10060;
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class PostService {
  private readonly _httpClient = inject(HttpClient);

  getUserPosts(userId: string) {
    return this._httpClient.get&lt;Post[]&gt;(`/api/posts/${userId}`);
  }
}</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;eb01c1e0-ffdf-4d40-bdf6-4f3fe125a99c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// After: @Service &#8212; clean and intentional &#9989;
import { Service, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Service()
export class PostService {
  private readonly _httpClient = inject(HttpClient);

  getUserPosts(userId: string) {
    return this._httpClient.get&lt;Post[]&gt;(`/api/posts/${userId}`);
  }
}</code></pre></div><p>The difference looks small in a single file, but across a codebase with hundreds of services, the intent becomes much clearer. <code>@Service()</code> says &#8220;this is a singleton service&#8221; without any ceremony.</p><p>When you don&#8217;t want automatic root-level providing &#8212; say, for a service scoped to a specific component or route &#8212; you opt out explicitly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;4328ae5d-b9e6-4d78-9af1-4c75abe2d4c0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">@Service({ autoProvided: false })
export class PanelStateService {
  private readonly _isOpen = signal(false);

  readonly isOpen = this._isOpen.asReadonly();

  toggle(): void {
    this._isOpen.update((open) =&gt; !open);
  }
}</code></pre></div><p>And if you need a custom factory (the equivalent of <code>useFactory</code> in <code>@Injectable</code>):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;726fa905-6f7d-4e1c-bf85-01db23546fb6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">@Service({
  factory: () =&gt; {
    const config = inject(AppConfig);
    const http = inject(HttpClient);
    return new ApiService(config.apiBaseUrl, http);
  },
})
export class ApiService {
  constructor(
    private readonly _baseUrl: string,
    private readonly _http: HttpClient
  ) {}

  get&lt;T&gt;(path: string) {
    return this._http.get&lt;T&gt;(`${this._baseUrl}${path}`);
  }
}</code></pre></div><p>One thing worth noting: <code>@Service</code> enforces <code>inject()</code>-only dependency resolution. If you try to use constructor parameters for DI, the compiler will throw a <code>SERVICE_CONSTRUCTOR_DI</code> error. This is intentional &#8212; the Angular team is pushing the ecosystem toward <code>inject()</code> as the standard pattern, and <code>@Service</code> draws that line clearly.</p><p><code>@Injectable</code> isn&#8217;t going anywhere. It&#8217;s still fully supported for cases that need its flexibility. But for new services, <code>@Service</code> is the cleaner, more opinionated choice.</p><h4>s) Testability Now Uses PendingTasks for Stability - <a href="https://github.com/angular/angular/pull/68187">PR</a></h4><p>Angular&#8217;s <code>Testability</code> service &#8212; the one that powers <code>whenStable()</code> for tools like Protractor and E2E testing frameworks &#8212; has been updated to use <code>PendingTasks</code> as its stability indicator. This is a significant step toward making the testing infrastructure work seamlessly in zoneless applications.</p><p>Here&#8217;s the backstory. Angular&#8217;s stability concept has historically been tied to Zone.js. The <code>Testability.isStable()</code> method checked whether the NgZone was stable and had no pending macrotasks. That worked fine when Zone.js was the only game in town, but it completely broke for zoneless apps &#8212; there was no Zone to check.</p><p>Meanwhile, Angular has been building <code>PendingTasks</code> as a zone-agnostic way to track async work. The <code>Router</code> and <code>HttpClient</code> already contribute to <code>PendingTasks</code>. This commit connects the dots: <code>Testability</code> now checks <code>PendingTasks</code> in addition to Zone stability.</p><p>The behavior depends on your setup:</p><ul><li><p><strong>Zoneless apps</strong> (no Zone.js present): <code>PendingTasks</code> is used automatically. <code>whenStable()</code> resolves when there are no pending tasks &#8212; no configuration needed.</p></li><li><p><strong>Zone.js apps</strong>: The old Zone-based stability check still works by default. You can opt into <code>PendingTasks</code>-based stability through <code>provideProtractorTestingSupport()</code>.</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;a636baff-0915-4dd9-b732-d6ae795481e2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Zoneless app &#8212; PendingTasks stability works automatically &#9989;
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideExperimentalZonelessChangeDetection(),
    // whenStable() now works correctly without Zone.js
  ],
};</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;362429b4-8c09-4df5-85f5-82bc2e3a1e5d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Zone.js app &#8212; opt into PendingTasks stability explicitly
import { provideProtractorTestingSupport } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideProtractorTestingSupport({
      usePendingTasksForStability: true,
    }),
  ],
};</code></pre></div><p>The <code>isStable()</code> method now evaluates three conditions instead of two:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;dec77770-2e3a-4415-9e6e-f230dd56b2f3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Internal logic (simplified)
isStable(): boolean {
  return (
    this._isZoneStable &amp;&amp;
    !this._ngZone.hasPendingMacrotasks &amp;&amp;
    (!this._usePendingTasks || !this.pendingTasksInternal.hasPendingTasks)
  );
}</code></pre></div><p>What this means for you in practice: if you&#8217;re migrating to zoneless and your E2E tests use <code>whenStable()</code> (directly or through a testing framework), they&#8217;ll now work correctly. The <code>Router</code> navigation, <code>HttpClient</code> requests, and any custom <code>PendingTasks</code> usage will all be tracked as part of application stability.</p><h4>t) <code>injectAsync</code> &#8212; lazy-load and inject services on demand - <a href="https://github.com/angular/angular/pull/68248">PR</a></h4><p>Here&#8217;s the thing about lazy loading in Angular: we&#8217;ve had lazy routes for ages, but lazy-loading <em>services</em> has always been awkward. You&#8217;d typically need to grab an <code>EnvironmentInjector</code>, dynamically import the service class, then manually call <code>injector.get()</code>. It worked, but it wasn&#8217;t ergonomic.</p><p><code>injectAsync</code> is a new helper function exported from <code>@angular/core</code> that streamlines this pattern. It returns a function that, when called, resolves to an instance of the lazily-loaded service &#8212; fully wired through DI.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;8294d697-7f58-4c30-96e2-478e0be34ec5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { Component, injectAsync } from '@angular/core';

@Component({
  selector: 'app-dashboard',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    &lt;button (click)="loadChart()"&gt;Show Chart&lt;/button&gt;
  `,
})
export class DashboardComponent {
  // Returns a function: () =&gt; Promise&lt;ChartService&gt;
  private _chartService = injectAsync(
    () =&gt; import('./chart.service').then((m) =&gt; m.ChartService),
  );

  async loadChart(): Promise&lt;void&gt; {
    const chartService = await this._chartService();
    chartService.render();
  }
}</code></pre></div><h4>u) SSR resource caching - <a href="https://github.com/angular/angular/commit/5a7c1e62dc2a4fa199b85150eca66914c107a6f4">PR</a></h4><p>Angular&#8217;s <code>resource()</code> and <code>rxResource()</code> now support a new <code>id</code> option that enables automatic caching through <code>TransferState</code>. This is the missing piece for SSR + signals: data fetched on the server gets serialized into the HTML, and when the client hydrates, the resource resolves <strong>synchronously</strong> from the cache instead of making a redundant network request.</p><p>Here&#8217;s the problem this solves. Without caching, your SSR flow looks like:</p><ol><li><p>Server renders &#8594; resource fetches data &#8594; HTML sent to client</p></li><li><p>Client hydrates &#8594; resource fetches the <strong>same data again</strong> &#8594; flash of loading state</p></li></ol><p>With the <code>id</code> option, step 2 becomes: client hydrates &#8594; resource reads from <code>TransferState</code> &#8594; <strong>instant resolution, no network call, no loading flicker</strong>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;9ad0adb1-6583-423d-9d06-b0d9dac0ff14&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { resource } from '@angular/core';
import { inject } from '@angular/core';

@Component({
  selector: 'app-product-detail',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (product.status() === 'resolved') {
      &lt;h1&gt;{{ product.value().name }}&lt;/h1&gt;
      &lt;p&gt;{{ product.value().description }}&lt;/p&gt;
    }
  `,
})
export class ProductDetailComponent {
  private _productId = input.required&lt;string&gt;();
  private _http = inject(HttpClient);

  product = resource({
    params: () =&gt; ({ id: this._productId() }),
    loader: ({ params }) =&gt;
      firstValueFrom(this._http.get&lt;Product&gt;(`/api/products/${params.id}`)),
    id: 'product-detail', // &#8592; enables TransferState caching
  });
}</code></pre></div><p>The <code>id</code> is a <code>string</code> added to <code>BaseResourceOptions</code>. It serves as the <code>TransferState</code> key. When the resource resolves on the server, the value is written to <code>TransferState</code> under that key. On the client, if a matching key exists in <code>TransferState</code>, the resource skips the loader entirely and resolves synchronously with the cached value.</p><p>This also works with <code>rxResource()</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;04b1230a-db16-4ec6-8059-fdd133563bee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { rxResource } from '@angular/core/rxjs-interop';

export class DashboardComponent {
  private _http = inject(HttpClient);

  metrics = rxResource({
    stream: () =&gt; this._http.get&lt;DashboardMetrics&gt;('/api/metrics'),
    id: 'dashboard-metrics',
  });
}</code></pre></div><p>This is different from <code>HttpClient</code>&#8216;s <code>transferCache</code> option (which caches HTTP responses at the interceptor level). The <code>id</code> option works at the <strong>resource level</strong>, meaning it caches the final resolved value regardless of how you fetched it &#8212; HTTP, WebSocket, IndexedDB, whatever your loader does.</p><p>One thing to note: you need <code>provideClientHydration()</code> in your app config for <code>TransferState</code> to work. If you&#8217;re already using SSR with hydration, you likely have this set up.</p><p></p><h3>Templates &amp; Compiler</h3><h4>a) Support comments in HTML elements - <a href="https://github.com/angular/angular/commit/e850643b1b8dca8cfdc12705be51441197cd987a">PR</a></h4><p>Angular&#8217;s template parser now supports HTML comments inside elements.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;603866da-09c2-46c0-a014-97bdc3836140&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;!-- This now works as expected everywhere in templates --&gt;
&lt;p&gt;Subscribe to codigotipado!&lt;/p&gt;</code></pre></div><h4>b) &#9888;&#65039; Compile-time diagnostic for duplicate selectors (NG8023) - <a href="https://github.com/angular/angular/commit/ca67828ee247bdff46736661e51f43f2ca736a24">PR</a></h4><p>Angular now throws a <strong>compile-time error</strong> when multiple components or directives share the same selector within the same scope. Before this, the behavior was undefined &#8212; sometimes one would win, sometimes you&#8217;d get subtle bugs that were painful to track down.</p><p>The new diagnostic <code>NG8023</code> catches this at build time:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;2ca7021c-bfd8-413c-930f-a62ca68e309f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// This now throws NG8023 at compile time
@Component({
  selector: &#8216;app-card&#8217;,
  template: `&lt;p&gt;Card A&lt;/p&gt;`,
})
export class CardAComponent {}

@Component({
  selector: &#8216;app-card&#8217;, // Duplicate selector!
  template: `&lt;p&gt;Card B&lt;/p&gt;`,
})
export class CardBComponent {}</code></pre></div><p><code>error NG8023: Multiple components match node with selector "app-card".</code></p><p>If you have duplicate selectors in your codebase, you&#8217;ll need to rename one of them. This is a breaking change, but honestly it&#8217;s catching a bug you already had.</p><h4>c) Warning for <code>@defer</code> prefetch without main trigger - <a href="https://github.com/angular/angular/commit/7f9450219f5c30d1ce0a90061864e8c844c8807c">PR</a></h4><p>The compiler now emits a <strong>warning</strong> when you use <code>prefetch</code> on a <code>@defer</code> block without specifying a main trigger. This catches a common mistake &#8212; if there&#8217;s no trigger to actually load the deferred content, the prefetch is pointless.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;3e4e4344-d6b6-45e1-bce1-4305b103087b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;!-- &#9888;&#65039; Warning: prefetch without a main trigger does nothing --&gt;
@defer (prefetch on idle) {
  &lt;app-heavy-widget /&gt;
}

&lt;!-- &#9989; Correct: has both a trigger and a prefetch strategy --&gt;
@defer (on viewport; prefetch on idle) {
  &lt;app-heavy-widget /&gt;
}</code></pre></div><p>This is a compile-time warning, not an error &#8212; your app will still build. But it&#8217;s a strong hint that something&#8217;s off in your defer configuration.</p><h4>d) &#9888;&#65039; Safe Navigation Now Correctly Narrows Nullables - <a href="https://github.com/angular/angular/pull/67959">PR</a></h4><p>This one's been a long time coming. Since 2020, Angular's safe navigation operator (<code>?.</code>) in templates didn't actually narrow types the way TypeScript does in regular code. That meant you couldn't rely on <code>?.</code> checks to eliminate <code>null | undefined</code> from subsequent expressions &#8212; the type system just ignored the narrowing.</p><p><strong>BREAKING CHANGE</strong>: This will trigger <code>nullishCoalescingNotNullable</code> and <code>optionalChainNotNullable</code> diagnostics on existing projects. If you're seeing a flood of new warnings after updating, you can temporarily disable those two diagnostics in your <code>tsconfig.json</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;492147a9-25dd-4206-83f0-cfc771fd2dea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">// tsconfig.json
{
  "angularCompilerOptions": {
    "extendedDiagnostics": {
      "checks": {
        "nullishCoalescingNotNullable": "suppress",
        "optionalChainNotNullable": "suppress"
      }
    }
  }
}</code></pre></div><p>Or, if you want to disable narrowing entirely (not recommended long-term):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;31ad1e7d-0cee-4df4-b4a1-a5ede9401a3b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "angularCompilerOptions": {
    "strictSafeNavigationTypes": false
  }
}</code></pre></div><p>Here&#8217;s what this actually means in practice:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;c4d21f9b-26d8-480c-9449-d95a0ac1c0e9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before: Angular didn't narrow after ?. &#8212; this compiled fine even though it's redundant
@Component({
  template: `
    @if (user?.name) {
      &lt;!-- user.name was still typed as string | undefined here --&gt;
      {{ user?.name ?? 'fallback' }}  &lt;!-- no warning, even though ?? is unnecessary --&gt;
    }
  `
})

// After &#9989;: Angular correctly narrows &#8212; you'll get a warning about the unnecessary ??
@Component({
  template: `
    @if (user?.name) {
      &lt;!-- user.name is now correctly narrowed to string --&gt;
      {{ user.name }}  &lt;!-- clean, no need for ?. or ?? --&gt;
    }
  `
})</code></pre></div><p>The <code>ng update</code> migration (covered below in Migrations) will automatically disable these diagnostics so you can update without being blocked, then fix them at your own pace.</p><h4>e) <strong>&#9888;&#65039;</strong> Optional chaining now returns <code>undefined</code> instead of <code>null</code> - <a href="https://github.com/angular/angular/pull/68084">PR</a></h4><p>This is a big one. Angular&#8217;s template compiler has historically treated the safe navigation operator (<code>?.</code>) differently from standard JavaScript/TypeScript. In vanilla JS, <code>foo?.bar</code> returns <code>undefined</code> when <code>foo</code> is nullish. But Angular templates used to return <code>null</code>. That inconsistency has been a source of subtle bugs for years &#8212; particularly when comparing values in templates or passing them to functions that distinguish between <code>null</code> and <code>undefined</code>.</p><p>Angular expressions with optional chaining now correctly return <code>undefined</code>, aligning template behavior with the JavaScript specification.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;1b494c6a-ca7f-4c5f-bf9e-33df7ca74769&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;!-- Given: user: { address?: { city?: string } } = {} --&gt;

&lt;!-- Before: this evaluated to null &#10060; --&gt;
&lt;!-- After: this evaluates to undefined &#9989; (matching JS behavior) --&gt;
{{ user?.address?.city }}

&lt;!-- This matters when you do strict comparisons --&gt;
@if (user?.address?.city === undefined) {
  &lt;p&gt;No city provided&lt;/p&gt;
}</code></pre></div><p><strong>Migration path:</strong> If you have code that explicitly checks for <code>null</code> from optional chaining results, you&#8217;ll need to update those checks. The Angular team provides two escape hatches:</p><ol><li><p><code>$null()</code><strong> magic function</strong> &#8212; wrap any expression to preserve the old behavior:</p></li></ol><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;2af54c28-cbfb-48bc-a3ac-ba960a8f8d39&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;!-- Opt-out: preserves legacy null-returning behavior --&gt;
{{ $null(user?.address?.city) }}</code></pre></div><ol start="2"><li><p><code>legacyOptionalChaining</code><strong> compiler option</strong> &#8212; a project-wide flag in <code>tsconfig.json</code>:</p></li></ol><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;f5c573f6-9ce5-4f8d-b4c7-39e224d5bc9c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "angularCompilerOptions": {
    "legacyOptionalChaining": true
  }
}</code></pre></div><p>The <code>legacyOptionalChaining</code> flag is meant as a temporary migration aid. It won&#8217;t stick around forever &#8212; plan to remove it once you&#8217;ve audited your templates.</p><p>What&#8217;s interesting is how the linker handles backward compatibility: libraries compiled with Angular &lt; 22 automatically get the legacy behavior applied, so you don&#8217;t need to worry about third-party packages breaking.</p><h4>f) Node.js 26.0.0 support - <a href="https://github.com/angular/angular/commit/b8d3f36ed962bd4f5abd6bf6e55078b56ce9fffa">PR</a></h4><h4>g) External TCBs with copied content in specific mode - <a href="https://github.com/angular/angular/commit/2eae497a04a6a9b34397181dcd64dbd103f76c47">PR</a></h4><p>Enables better type-checking accuracy when working with templates in external files.</p><p></p><h3>Forms</h3><p>Signal Forms got a lot of love in this release. <a href="https://www.codigotipado.com/p/mastering-angular-21-signal-forms">Learn more about Angular Signal Forms here</a>.</p><h4>a) <code>debounce()</code> with &#8216;blur&#8217; option - <a href="https://github.com/angular/angular/commit/c767d678cff65a89f380b4512590fa732db072c8">PR</a></h4><p>The <a href="https://www.codigotipado.com/p/mastering-the-new-debounce-api-in">debounce() rule in Signal Forms</a> now supports a <code>'blur'</code> mode. Instead of debouncing by time, validation only fires when the field loses focus:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;d27c7545-2de5-45f8-aee9-85a3b3093055&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { form, debounce, validateAsync } from &#8216;@angular/forms/signals&#8217;;

const myForm = form(data, (path) =&gt; {
  // Validation fires only when the user leaves the field
  debounce(path.username, &#8216;blur&#8217;);

  validateAsync(path.username, {
    params: (ctx) =&gt; ({ username: ctx.value() }),
    factory: (params) =&gt; httpResource(() =&gt; `/api/check-username?u=${params.username}`),
    onSuccess: (result) =&gt; (result.taken ? { usernameTaken: true } : null),
  });
});</code></pre></div><p>This is perfect for fields where partial values are meaningless &#8212; IBANs, coupon codes, usernames. No point validating &#8220;Lond&#8221; against an API when the user clearly isn&#8217;t done typing.</p><h4>b) <code>reloadValidation()</code> for Signal Forms - <a href="https://github.com/angular/angular/commit/74f76d8075d03b1271aef37b974c9e15f9c7d3af">PR</a></h4><p>You can now manually trigger async validation on a signal form field with <code>reloadValidation()</code>. This is useful when external state changes and you need to re-validate without the user modifying the field &#8212; think of a coupon code field that should re-validate when the cart contents change.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;b6c6f724-b1cc-4d6b-be42-df35be7ac73c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { form, validateAsync, reloadValidation } from &#8216;@angular/forms/signals&#8217;;

@Component({
  selector: &#8216;app-checkout&#8217;,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    &lt;input [formField]=&#8221;couponField&#8221; placeholder=&#8221;Coupon code&#8221; /&gt;
    @if (couponField.errors()?.invalidCoupon) {
      &lt;span&gt;This coupon is no longer valid for your cart&lt;/span&gt;
    }
    &lt;button (click)=&#8221;onCartChanged()&#8221;&gt;Recalculate&lt;/button&gt;
  `,
})
export class CheckoutComponent {
  private readonly _cartService = inject(CartService);

  protected readonly data = signal({ coupon: &#8216;&#8217; });

  protected readonly checkoutForm = form(this.data, (path) =&gt; {
    validateAsync(path.coupon, {
      params: (ctx) =&gt; ({ code: ctx.value(), cartTotal: this._cartService.total() }),
      factory: (params) =&gt;
        httpResource(() =&gt; `/api/validate-coupon?code=${params.code}&amp;total=${params.cartTotal}`),
      onSuccess: (result) =&gt; (result.valid ? null : { invalidCoupon: true }),
    });
  });

  protected get couponField() {
    return this.checkoutForm.controls.coupon;
  }

  onCartChanged(): void {
    // Cart changed &#8212; re-validate the coupon without the user touching the field
    reloadValidation(this.checkoutForm.controls.coupon);
  }
}</code></pre></div><h4>c) Debounce option for <code>validateAsync</code> and <code>validateHttp</code> - <a href="https://github.com/angular/angular/commit/24e52d450d201e3da90bb64f84358f9eccd7877d">PR</a></h4><p>Async validators now accept a debounce configuration directly, so you don&#8217;t always need a separate <code>debounce()</code> rule:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;ecf05a3f-c3dc-46e1-8b2e-e21cd46fb01c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// validateAsync with built-in debounce
validateAsync(path.city, {
  debounce: 500,
  params: (ctx) =&gt; ({ city: ctx.value() }),
  factory: (params) =&gt; httpResource(() =&gt; `/api/validate-city?name=${params.city}`),
  onSuccess: (result) =&gt; (result.valid ? null : { invalidCity: true }),
});

// validateHttp with built-in debounce &#8212; same pattern
validateHttp(path.email, {
  debounce: 300,
  request: (ctx) =&gt; `/api/check-email?email=${encodeURIComponent(ctx.value())}`,
  onSuccess: (response) =&gt; (response.available ? null : { emailTaken: true }),
});</code></pre></div><p>Before this, you had to pair every async validator with a separate <code>debounce()</code> call. Now it&#8217;s a single declaration. The <code>debounce</code> option accepts a number (milliseconds) or <code>'blur'</code> &#8212; same as the standalone <code>debounce()</code> rule.</p><h4>d) <code>FieldState.getError()</code> - <a href="https://github.com/angular/angular/commit/709f5a390ca0de04f8066012a5cb36999f2fd4a6">PR</a></h4><p>A new convenience method to retrieve a specific error from a field&#8217;s validation state:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;ef0b36c9-6ea6-471f-88fb-f19ccc750f7d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">const emailError = fieldState.getError(&#8217;email&#8217;);
// Returns the error value or undefined</code></pre></div><h4>e) Support binding <code>number | null</code> to <code>&lt;input type="text"&gt;</code> - <a href="https://github.com/angular/angular/commit/41b1410cb8a333a2ce6569483cd10866effc154d">PR</a></h4><p>Signal Forms now properly handle binding <code>number | null</code> values to text inputs. Before this, you&#8217;d get type errors or unexpected behavior when a numeric field could be null.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;2f3077ef-c577-48e0-aedd-62561d8c3f45&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">type ProductForm = {
  name: string;
  price: number | null;  // nullable numeric field
  discount: number | null;
};

const data = signal&lt;ProductForm&gt;({
  name: &#8216;Widget&#8217;,
  price: 29.99,
  discount: null, // no discount yet
});

const productForm = form(data, (path) =&gt; {
  validate(path.name, required);
});</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;03ec02c1-13ce-4f26-8f5a-39f820645228&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;!-- This now works without type errors --&gt;
&lt;input type=&#8221;text&#8221; [formField]=&#8221;productForm.controls.price&#8221; /&gt;
&lt;input type=&#8221;text&#8221; [formField]=&#8221;productForm.controls.discount&#8221; /&gt;</code></pre></div><p>When the value is <code>null</code>, the input displays empty. When the user types a number, it&#8217;s parsed back to <code>number</code>. Clean round-trip for nullable numeric fields.</p><h4>f) <code>ngNoCva</code> opt-out for ControlValueAccessors - <a href="https://github.com/angular/angular/commit/3983080236e348ecc17ab4e65a6a5cc0a16aa315">PR</a></h4><p>A new <code>ngNoCva</code> attribute lets you opt out of ControlValueAccessor binding on specific elements. This is useful when you have a custom form control but don&#8217;t want Angular&#8217;s CVA machinery to interfere with certain child elements.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;html&quot;,&quot;nodeId&quot;:&quot;052f94b3-1402-4fa4-8690-dfb5e681225b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-html">&lt;!-- This input won&#8217;t get a ControlValueAccessor attached --&gt;
&lt;input ngNoCva type=&#8221;text&#8221; /&gt;</code></pre></div><h4>g) Template &amp; reactive support for FVC - <a href="https://github.com/angular/angular/commit/c4ce3f345fdb14595f0991dff488c4043a0fc71c">PR</a></h4><p>Form Validation Controls (FVC) now work with both template-driven and reactive forms approaches, expanding the interop surface between Signal Forms and the existing form systems.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;0cbd4f84-bc0f-40bb-8bcc-0904f77440d1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Template-driven: FVC works with ngModel
@Component({
  selector: &#8216;app-profile&#8217;,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [FormsModule, SignalFormsModule],
  template: `
    &lt;input [(ngModel)]=&#8221;name&#8221; [formValidation]=&#8221;nameValidation&#8221; /&gt;
    @if (nameValidation.errors()?.required) {
      &lt;span&gt;Name is required&lt;/span&gt;
    }
  `,
})
export class ProfileComponent {
  protected name = &#8216;&#8217;;
  protected readonly nameValidation = fieldValidation(/* ... */);
}

// Reactive: FVC works with FormControl
@Component({
  selector: &#8216;app-settings&#8217;,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ReactiveFormsModule, SignalFormsModule],
  template: `
    &lt;input [formControl]=&#8221;emailControl&#8221; [formValidation]=&#8221;emailValidation&#8221; /&gt;
  `,
})
export class SettingsComponent {
  protected readonly emailControl = new FormControl(&#8217;&#8216;);
  protected readonly emailValidation = fieldValidation(/* ... */);
}</code></pre></div><h4>h) &#9889; Lazily instantiate signal form fields - <a href="https://github.com/angular/angular/commit/98c5afdb02192f99c886fc3fda13ec6f39018f23">PR</a></h4><p>A performance improvement that defers the instantiation of signal form fields until they&#8217;re actually needed. For large forms with many fields, this reduces the initial creation cost.</p><h4>i) Shim Legacy NG_VALIDATORS into parseErrors for CVA Mode - <a href="https://github.com/angular/angular/pull/67943">PR</a></h4><p>Signal Forms CVA (ControlValueAccessor) interop mode now properly bridges legacy <code>NG_VALIDATORS</code> into the new <code>parseErrors</code> system. If you have existing custom validators registered through the legacy <code>NG_VALIDATORS</code> token, they&#8217;ll now work correctly when used alongside Signal Forms CVA mode.</p><p>This is about backward compatibility. As the Angular team builds out Signal Forms, they need to ensure that existing form infrastructure doesn&#8217;t break. This shim means you can incrementally adopt Signal Forms without rewriting all your custom validators at once:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;5b485a07-4283-47a4-91d4-e8806dc781c6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Your existing validator directive still works with Signal Forms CVA mode
@Directive({
  selector: '[appCustomValidator]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: CustomValidatorDirective,
      multi: true,
    },
  ],
})
export class CustomValidatorDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    return control.value?.length &gt; 10 ? { tooLong: true } : null;
  }
}

// These legacy validators now correctly surface in Signal Forms' parseErrors
// No migration needed &#8212; the shim handles the bridging automatically</code></pre></div><h4>j) &#127881; Signal forms APIs graduate to public API - <a href="https://github.com/angular/angular/commit/7745365910771d97c91e9b640c2c26a99bfa5a6d">PR</a></h4><p>This is the big one. Signal-based forms &#8212; which have been in developer preview &#8212; are now part of Angular's stable public API. This means the APIs are subject to Angular's semantic versioning guarantees and won't have breaking changes outside major versions.</p><p><a href="https://www.codigotipado.com/p/mastering-angular-21-signal-forms">Read full guide about Angular Signal Forms here</a>.</p><h4>k) &#9889; Performance: shallow array equality for reactivity - <a href="https://github.com/angular/angular/commit/e0536091f5f6c2033e377998eea3bf65b14f5ac6">PR</a></h4><p>Signal forms now use shallow array equality checks when determining whether reactive values have changed. Previously, any mutation to an array (even if the contents were identical) would trigger downstream recomputation. Now, if the array elements are the same by reference, the signal won&#8217;t notify dependents.</p><p>This reduces unnecessary change detection cycles in forms with array-based controls (like dynamic field lists or multi-select values).</p><h4>l) &#9889; Performance: shortcut deepSignal writes if value is unchanged - <a href="https://github.com/angular/angular/commit/9b9769479b295bf34bae9a938ee758a256bd4b32">PR</a></h4><p>When writing to a deep signal (nested form state), the framework now short-circuits if the new value is identical to the current one. No notification, no recomputation, no template re-render. This is a targeted optimization for forms where programmatic resets or patches might write the same value back.</p><h4>m) <strong>&#9888;&#65039;</strong> <code>min</code> and <code>max</code> validation rules no longer accept strings - <a href="https://github.com/angular/angular/pull/68001">PR</a></h4><p>The <code>min</code> and <code>max</code> validation rules in signal forms no longer accept string values. Bound values must be <code>number | null</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;2db099c2-7522-4c07-a9ae-d55ee04ada03&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#8212; strings were silently accepted &#10060;
this._fb.field(0, { validators: [min('5')] });

// After &#8212; must be number or null &#9989;
this._fb.field(0, { validators: [min(5)] });</code></pre></div><p>If you were passing string values (perhaps from template bindings), you&#8217;ll need to parse them to numbers first.</p><p></p><h3>HTTP</h3><h4>a) &#9888;&#65039; FetchBackend is now the default HTTP backend - <a href="https://github.com/angular/angular/commit/5c432fb8bb69343ef2633811c37c0c6c0fd65126">PR</a></h4><p><code>provideHttpClient()</code> now uses <code>FetchBackend</code> by default instead of <code>HttpXhrBackend</code>. The Fetch API is more modern, supports streaming, and aligns with where the web platform is heading.</p><p><strong>What this means:</strong></p><ul><li><p>Upload progress reports are <strong>no longer available</strong> by default (Fetch API limitation)</p></li><li><p>If you need upload progress, use <code>withXhr()</code>:</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;da83b233-40f0-4f15-82ee-7df0acb66854&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before (Angular 21) &#8212; XHR was the default
provideHttpClient()

// Angular 22 &#8212; Fetch is the default. Same call, different backend.
provideHttpClient()

// If you need XHR (for upload progress):
provideHttpClient(withXhr())</code></pre></div><p>There&#8217;s a migration schematic that adds <code>withXhr()</code> where needed. <code>withFetch()</code> is now <strong>deprecated</strong> since Fetch is the default &#8212; you can safely remove it.</p><h4>b) <code>reportUploadProgress</code> &amp; <code>reportDownloadProgress</code> replace <code>reportProgress</code> - <a href="https://github.com/angular/angular/commit/7c8c3347efc1be2b5967b9481e3a2a3a23c24977">commit 7c8c334</a></h4><p>The monolithic <code>reportProgress</code> option on HTTP requests is now deprecated. In its place, two granular options give you explicit control over which direction of progress you want to track:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;e5502376-ca23-4a37-aff7-ac237efea495&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#8212; reported both upload AND download progress &#10060; (deprecated)
this._http.post('/upload', formData, {
  reportProgress: true,
  observe: 'events',
});

// After &#8212; explicit about what you're tracking &#9989;
this._http.post('/upload', formData, {
  reportUploadProgress: true,
  observe: 'events',
});

// Or for downloads:
this._http.get('/large-file', {
  reportDownloadProgress: true,
  observe: 'events',
});

// Or both:
this._http.post('/upload-with-response', formData, {
  reportUploadProgress: true,
  reportDownloadProgress: true,
  observe: 'events',
});</code></pre></div><p>This split makes intent clearer and avoids the overhead of tracking progress in a direction you don&#8217;t care about. Most file upload UIs only need upload progress; most download UIs only need download progress. Now you can be explicit.</p><p></p><h3>Router</h3><h4>a) <code>withComponentInputBinding</code> options - <a href="https://github.com/angular/angular/commit/89c9a4de308a087ce95246ee259f32c8a927e39e">PR</a></h4><p><code>withComponentInputBinding()</code> now accepts an optional config to control which router sources bind to your component inputs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;a198a85c-17e6-4adc-a21f-508dab32fc69&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Disable query param binding &#8212; only path params and route data bind
provideRouter(routes, withComponentInputBinding({ queryParams: false }));</code></pre></div><p>Before this, it was all or nothing. Now you can prevent query parameters from overriding your component inputs &#8212; which is a real hygiene improvement since query params are user-controlled. I wrote a full article about this one: <strong><a href="https://www.codigotipado.com/p/you-can-now-control-what-binds-to">Selective Component Input Binding in Angular 22</a></strong>.</p><h4>b) <code>browserUrl</code> input support for router links - <a href="https://github.com/angular/angular/commit/3683902234acf74c7047337bda4db937e93f93d7">PR</a></h4><p>Router links now support a <code>browserUrl</code> input that lets you display a different URL in the browser&#8217;s address bar than the actual route being navigated to. This was already possible with <code>router.navigateByUrl()</code> via <code>NavigationBehaviorOptions</code>, but now it works declaratively in templates.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;b4dad057-ef9a-44db-ab34-0a3827415a2c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">@Component({
  selector: &#8216;app-product-list&#8217;,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [RouterLink],
  template: `
    @for (product of products(); track product.id) {
      &lt;!-- Navigates to /products/42/details internally,
           but the browser shows /products/cool-widget --&gt;
      &lt;a
        [routerLink]=&#8221;[&#8217;/products&#8217;, product.id, &#8216;details&#8217;]&#8221;
        [browserUrl]=&#8221;&#8217;/products/&#8217; + product.slug&#8221;
      &gt;
        {{ product.name }}
      &lt;/a&gt;
    }
  `,
})
export class ProductListComponent {
  products = input.required&lt;Product[]&gt;();
}</code></pre></div><p>The <code>browserUrl</code> input accepts a <code>string</code> or <code>UrlTree</code>. This is useful for SEO-friendly URLs, vanity URLs, or when your internal route structure doesn&#8217;t match the URL you want users to see and share. It works on both <code>&lt;a&gt;</code> elements and non-anchor elements with <code>[routerLink]</code>.</p><h4>c) &#9888;&#65039; <code>provideRoutes()</code> removed - <a href="https://github.com/angular/angular/commit/bdb6ae9dbc080cd6ce4f5058c65f6b2bd853beda">PR bdb6ae9</a></h4><p>The deprecated <code>provideRoutes()</code> function has been removed. Use <code>provideRouter()</code> or the <code>ROUTES</code> multi token instead:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;b31d3718-137b-4c92-9445-c264ea69414a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060;
provideRoutes(myRoutes)

// After &#9989;
provideRouter(myRoutes)

// Or if you need the multi-token approach:
{ provide: ROUTES, useValue: myRoutes, multi: true }</code></pre></div><h4>d) <code>unmatchedInputBehavior</code> option for <code>componentInputBinding</code> - <a href="https://github.com/angular/angular/commit/c84642ac16bf3588c071bbdcc684daa8d4e494b3">PR</a></h4><p>When you enable <code>withComponentInputBinding()</code> in the router, route parameters and query params are automatically bound to component inputs. But what happens when a route parameter doesn&#8217;t match any input on the component? Previously, it was silently ignored.</p><p>The new <code>unmatchedInputBehavior</code> option lets you control this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;e457d668-4e14-493d-94d2-e00401979b43&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { provideRouter, withComponentInputBinding } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding({ unmatchedInputBehavior: 'warn' }),
    ),
  ],
};</code></pre></div><p>This is useful for catching typos in route configurations or detecting when route params change names but components haven&#8217;t been updated. In development, setting this to <code>'warn'</code> or <code>'error'</code> helps catch mismatches early.</p><p></p><h3>Rendering &amp; Platform</h3><h4>a) &#9888;&#65039; Hammer.js integration removed - <a href="https://github.com/angular/angular/commit/f99e7ed20f0b1a26fd275fcf5befd589bb4e5d31">PR</a></h4><p>The built-in Hammer.js integration has been completely removed. If you&#8217;re using gesture events like <code>(swipe)</code>, <code>(pinch)</code>, <code>(rotate)</code>, etc., you&#8217;ll need to implement your own gesture handling or use a third-party library directly.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;e990bd1f-77cd-4b05-860f-749ccb3ea695&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060; &#8212; Angular handled Hammer.js integration
import { HammerModule } from &#8216;@angular/platform-browser&#8217;;

// After &#9989; &#8212; Use Hammer.js directly or switch to Pointer Events
// Install hammerjs yourself and set it up manually,
// or migrate to native pointer events</code></pre></div><p>For most apps, the <strong><a href="https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events">Pointer Events API</a></strong> is the modern replacement.</p><h4>b) Incremental Hydration Is Now the Default - <a href="https://github.com/angular/angular/pull/68092">PR</a></h4><p>Incremental hydration is no longer opt-in &#8212; it&#8217;s the default behavior. Previously, you had to explicitly enable it. Now, when your application uses SSR with hydration, Angular will incrementally hydrate components as they become needed rather than hydrating the entire page at once.</p><p>What does this mean in practice? Your SSR pages will become interactive faster because Angular only hydrates the components that the user is actually interacting with. A component at the bottom of a long page won&#8217;t be hydrated until the user scrolls to it (or it&#8217;s triggered by some other interaction).</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;21ff0350-3ff7-4828-bfa2-e9be4567555e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before: you had to explicitly opt in
// provideClientHydration(withIncrementalHydration())

// After &#9989;: incremental hydration is on by default
// Just use provideClientHydration() &#8212; incremental behavior is included
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideClientHydration(),  // incremental hydration is now default
  ],
};</code></pre></div><p>You can still use <code>@defer</code> blocks to control hydration triggers:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;c15b5f32-77aa-4568-8032-9d3d23c8ef50&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">@Component({
  template: `
    &lt;header&gt;
      &lt;app-nav /&gt;
    &lt;/header&gt;

    &lt;main&gt;
      &lt;app-hero-section /&gt;

      @defer (on viewport) {
        &lt;app-comments-section /&gt;  &lt;!-- Only hydrates when scrolled into view --&gt;
      }

      @defer (on interaction) {
        &lt;app-feedback-form /&gt;  &lt;!-- Only hydrates when user interacts --&gt;
      }
    &lt;/main&gt;
  `
})
export class PageComponent {}</code></pre></div><p>This is a meaningful performance win for content-heavy pages. The initial Time to Interactive (TTI) drops because Angular isn&#8217;t spending time hydrating components the user hasn&#8217;t reached yet.</p><p></p><h3>Developer Tooling</h3><h4>a) Angular DI Graph In-Page AI Tool - <a href="https://github.com/angular/angular/pull/68030">PR</a></h4><p>Angular now ships a built-in AI tool that exposes the dependency injection graph at runtime. This is part of the broader push to make Angular applications introspectable by AI assistants and developer tools.</p><p>The DI graph tool lets AI-powered debugging assistants (think: Copilot, Gemini, or custom MCP tools) query the injector hierarchy, understand which services are provided where, and trace dependency chains &#8212; all from within the running application.</p><h4>b) Register AI Runtime Debugging Tools - <a href="https://github.com/angular/angular/pull/67985">PR</a></h4><p>Closely related to the DI graph tool above, this commit establishes the infrastructure for registering AI runtime debugging tools in Angular. It provides the foundation that allows Angular to expose runtime information (component trees, signal states, DI graphs) to external AI tools.</p><p>Think of it as the plumbing that makes the DI graph tool (and future AI tools) possible. The framework now has a standardized way to register and expose debugging capabilities that AI assistants can consume.</p><h4>c) Enhanced Profiling with Documentation URLs - <a href="https://github.com/angular/angular/pull/67942">PR</a></h4><p>Angular&#8217;s profiling output now includes links to relevant documentation. When you&#8217;re profiling your application and see a particular operation taking time, the profiler will point you directly to the docs that explain what that operation does and how to optimize it.</p><p>It&#8217;s a small but thoughtful improvement. Instead of seeing a cryptic profiling label and having to search the docs yourself, you get a direct link. This is especially helpful for developers who are newer to Angular&#8217;s internals.</p><h4>d) <code>provideWebMcpTools</code> &#8212; MCP integration for Angular apps - <a href="https://github.com/angular/angular/pull/68139">PR</a></h4><p>Angular is going all-in on AI tooling. <code>provideWebMcpTools</code> is a new provider function that registers <a href="https://anthropic.com/news/model-context-protocol">WebMCP</a> tools directly through Angular&#8217;s dependency injection system. MCP (Model Context Protocol) is the open standard that lets AI assistants &#8212; Claude, Copilot, Cursor, etc. &#8212; interact with your running application: inspecting component state, triggering actions, querying data.</p><p>Here&#8217;s what makes this interesting: the tools are <strong>tied to the lifecycle of the </strong><code>Injector</code> they&#8217;re provided to. They register automatically when the environment initializes and unregister when the injector is destroyed. And the <code>execute</code> function runs in the injection context of that injector &#8212; meaning you can <code>inject()</code> any dependency inside it.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;08838c96-6d56-46aa-bd9b-2a4b47c4dbe0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { provideWebMcpTools } from '@angular/core';
import { ApplicationConfig } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideWebMcpTools([
      {
        name: 'getUserCart',
        description: 'Returns the current user cart items with quantities and prices',
        inputSchema: { type: 'object', properties: {} },
        execute: async () =&gt; {
          const cartService = inject(CartService);
          const items = cartService.items();

          return {
            content: [{
              type: 'text',
              text: JSON.stringify(items),
            }],
          };
        },
      },
    ]),
  ],
};</code></pre></div><p>The <code>execute</code> function receives two arguments: the parsed <code>args</code> (typed from your <code>inputSchema</code>) and a <code>WebMcpClient</code> object containing an <code>AbortSignal</code> for cancellation support.</p><p>What&#8217;s particularly powerful is combining this with <strong>route-level providers</strong> and <code>withExperimentalAutoCleanupInjectors</code>. Tools register when the user navigates to a route and automatically unregister when navigating away:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;32c7fc3a-c45f-426e-8894-5ed8f1f860be&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { provideRouter, withExperimentalAutoCleanupInjectors } from '@angular/router';
import { provideWebMcpTools } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      [
        {
          path: 'admin',
          component: AdminDashboardComponent,
          providers: [
            provideWebMcpTools([
              {
                name: 'getSystemMetrics',
                description: 'Returns current system health metrics (only available on admin page)',
                inputSchema: {
                  type: 'object',
                  properties: {
                    timeRange: { type: 'string', enum: ['1h', '24h', '7d'] },
                  },
                },
                execute: async (args) =&gt; {
                  const metrics = inject(MetricsService);
                  const data = await metrics.fetch(args.timeRange);

                  return { content: [{ type: 'text', text: JSON.stringify(data) }] };
                },
              },
            ]),
          ],
        },
      ],
      withExperimentalAutoCleanupInjectors(),
    ),
  ],
};</code></pre></div><p>When the user navigates to <code>/admin</code>, the <code>getSystemMetrics</code> tool becomes available to AI assistants. Navigate away, and it&#8217;s gone. No manual cleanup needed.</p><p>Under the hood, <code>provideWebMcpTools</code> is a thin wrapper &#8212; it calls <code>makeEnvironmentProviders</code> with a <code>provideEnvironmentInitializer</code> that iterates over your tool descriptors and calls <code>declareWebMcpTool</code> for each one.</p><h4>e) <code>declareWebMcpTool</code> &#8212; the low-level imperative API - <a href="https://github.com/angular/angular/pull/68139">PR</a></h4><p>While <code>provideWebMcpTools</code> is the ergonomic, declarative approach, <code>declareWebMcpTool</code> is the lower-level primitive it&#8217;s built on. This function immediately registers a single WebMCP tool and ties its lifecycle to the current (or provided) <code>Injector</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;ea065504-9526-424c-9f41-e16426f11836&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { declareWebMcpTool } from '@angular/core';
import type { WebMcpToolDescriptor } from '@angular/core';

@Component({
  selector: 'app-chat',
  template: `&lt;div class="chat-container"&gt;...&lt;/div&gt;`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChatComponent {
  private _chatService = inject(ChatService);

  constructor() {
    // Registers the tool in the current injection context.
    // Automatically unregistered when this component's injector is destroyed.
    declareWebMcpTool({
      name: 'sendMessage',
      description: 'Sends a message in the active chat conversation',
      inputSchema: {
        type: 'object',
        properties: {
          message: { type: 'string' },
          channel: { type: 'string' },
        },
      },
      execute: async (args) =&gt; {
        await this._chatService.send(args.channel, args.message);
        return { content: [{ type: 'text', text: `Message sent to #${args.channel}` }] };
      },
    });
  }
}</code></pre></div><p>The function signature:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;70cd432b-f34e-4d27-8788-883c8ac6849f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">declare function declareWebMcpTool&lt;const InputSchema extends JsonSchemaForInference&gt;(
  tool: WebMcpToolDescriptor&lt;InputSchema&gt;,
  injector?: Injector,
): void;</code></pre></div><p>The optional <code>injector</code> parameter lets you register tools outside an injection context &#8212; useful for dynamic scenarios where you create tools programmatically.</p><p><strong>Key types exported:</strong></p><ul><li><p><code>WebMcpToolDescriptor&lt;InputSchema&gt;</code> &#8212; the tool definition (name, description, inputSchema, execute)</p></li><li><p><code>WebMcpToolExecute&lt;InputSchema&gt;</code> &#8212; the execute function type: <code>(args: InferArgsFromInputSchema&lt;InputSchema&gt;, client: WebMcpClient) =&gt; unknown</code></p></li><li><p><code>WebMcpClient</code> &#8212; contains <code>signal: AbortSignal</code> for cancellation</p></li></ul><p><strong>When to use which:</strong></p><ul><li><p><code>provideWebMcpTools</code> &#8594; declarative, multiple tools at once, works in <code>providers</code> arrays</p></li><li><p><code>declareWebMcpTool</code> &#8594; imperative, single tool, useful inside constructors or <code>afterNextRender</code></p></li></ul><p>Both are marked <code>@experimental</code>. The JSON Schema types are intentionally not exported from <code>@angular/core</code> &#8212; if you need them for complex schemas, add <code>@mcp-b/webmcp-types</code> as a direct dependency.</p><p></p><h3>Language Tooling</h3><h4>a) Template inlay hints - <a href="https://github.com/angular/angular/commit/5a6d88626b604db937287a501cb723c088412a7e">PR</a></h4><p>The Angular Language Service now supports <strong>inlay hints</strong> in templates. These are the subtle inline annotations your editor shows for parameter names, types, and other contextual information. If you use VS Code with the Angular extension, you&#8217;ll start seeing type hints directly in your templates.</p><h4>b) Document Symbols for Angular templates - <a href="https://github.com/angular/angular/commit/cfd0f9950c08324e1c56f16d98a2e3081feeda58">PR</a></h4><p>The language service now provides Document Symbols for Angular templates. This means the <strong>Outline view</strong> in VS Code (and other editors) will show the structure of your template &#8212; components, directives, control flow blocks &#8212; making navigation in large templates much easier.</p><h4>c) Idle timeout support in defer blocks - <a href="https://github.com/angular/angular/commit/c6f98c723cdd2c209092927855f8cbaf63ecce30">PR</a></h4><p>The language service now understands the new idle timeout syntax in <code>@defer</code> blocks, providing proper autocompletion and diagnostics.</p><p></p><h3>Legacy / Upgrade</h3><h4>a) &#9888;&#65039; <code>getAngularLib</code>/<code>setAngularLib</code> removed - <a href="https://github.com/angular/angular/commit/01a179577b5a250f5801f6d9a04378aea73c4251">PR</a></h4><p>The deprecated <code>getAngularLib</code> and <code>setAngularLib</code> functions have been removed. Use <code>getAngularJSGlobal</code> and <code>setAngularJSGlobal</code> instead:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;095c7263-6c61-48c0-93c4-362fe0075ddb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060;
import { getAngularLib, setAngularLib } from &#8216;@angular/upgrade/static&#8217;;

// After &#9989;
import { getAngularJSGlobal, setAngularJSGlobal } from &#8216;@angular/upgrade/static&#8217;;</code></pre></div><p></p><h2>&#128736;&#65039; CLI &amp; Build Tooling (<code>angular/angular-cli</code>)</h2><h3>@angular/cli</h3><h4>a) &#9889; Faster <code>ng add</code> with cached resolution - <a href="https://github.com/angular/angular-cli/commit/ad0fd5f41fc6ee5d920fb3c725f09f17d86d2ab4">PR</a> &amp; <a href="https://github.com/angular/angular-cli/commit/a39a33128fb56e9c65ea89e06c4f127252d3b220">PR</a></h4><p>Two performance improvements to <code>ng add</code>: redundant package version resolution is now avoided, and the root manifest is cached with restricted package exports resolved upfront. If you&#8217;ve ever noticed <code>ng add</code> being slow on large monorepos, this should help.</p><h4>b) MCP devserver custom port support - <a href="https://github.com/angular/angular-cli/commit/87d99e98b178c8f7d5e944a346faf70c51bdfcef">PR</a></h4><p>The MCP (Model Context Protocol) devserver start tool now supports a custom port. This is relevant if you&#8217;re using Angular&#8217;s AI tooling integration.</p><h4>c) Chunk Optimization Enabled by Default with Smart Heuristics - <a href="https://github.com/angular/angular-cli/commit/cd2ad3c1958d9adadc214045b5e59c0c8f8b4499">PR</a></h4><p>The advanced chunk optimization pass &#8212; which uses Rollup (or optionally Rolldown) to re-bundle lazy chunks after esbuild &#8212; is now enabled by default. Previously, you had to explicitly opt in via the <code>NG_BUILD_OPTIMIZE_CHUNKS</code> environment variable. That&#8217;s no longer the case.</p><p>Here&#8217;s the thing: not every project benefits from this. A small app with one or two lazy routes won&#8217;t see meaningful gains, and the extra build step just adds overhead. So the Angular team introduced a heuristic: the optimization only kicks in when your build produces <strong>3 or more lazy chunks</strong>. This threshold hits the sweet spot where chunk merging and deduplication actually pay off.</p><p>The heuristic counts <code>.js</code> files in the build output that aren&#8217;t part of the initial bundle. If that count meets the threshold, the optimization runs automatically. For most medium-to-large apps with multiple lazy-loaded routes, this means smaller total bundle sizes and fewer network requests &#8212; without you having to configure anything.</p><p>You can still control the behavior through the <code>NG_BUILD_OPTIMIZE_CHUNKS</code> environment variable:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;46b7541e-9a0f-458c-8f0e-a04b25cb8b87&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash"># Force optimization on regardless of chunk count
NG_BUILD_OPTIMIZE_CHUNKS=true ng build

# Disable optimization entirely
NG_BUILD_OPTIMIZE_CHUNKS=false ng build

# Set a custom threshold (e.g., only optimize when 5+ lazy chunks exist)
NG_BUILD_OPTIMIZE_CHUNKS=5 ng build</code></pre></div><p>What happens under the hood: after esbuild generates the initial chunks, Angular feeds them through Rollup (or Rolldown if you&#8217;ve opted into it via <code>NG_BUILD_CHUNKS_ROLLDOWN</code>). Rollup analyzes cross-chunk dependencies and merges small chunks that share code, reducing the total number of files the browser needs to fetch for lazy routes.</p><p>For a typical app with 10+ lazy routes, this can reduce the number of JS files by 30-50%, which translates directly to fewer HTTP requests and faster route transitions. The build time increase is minimal &#8212; usually under a second for the optimization pass itself.</p><h4>d) Quiet Option for Suppressing Build Noise in Unit Tests - <a href="https://github.com/angular/angular-cli/commit/0781f74986e773d143b9bdf5d3f0747de0df6b0a">PR</a></h4><p>A new <code>quiet</code> option has been added to <code>@angular/build</code> that suppresses build output noise during unit test runs. When running Vitest through the Angular builder, the build system now supports silencing the verbose compilation output that clutters test results.</p><p>This is a developer experience improvement. When you&#8217;re running tests, you want to see test results &#8212; not pages of build logs scrolling by. The <code>quiet</code> option lets the test runner focus the terminal output on what matters: which tests passed, which failed, and why.</p><h4>e) Stabilized Jasmine-to-Vitest Migration Schematic - <a href="https://github.com/angular/angular-cli/commit/de630c2fcee22622942478c98117d8f473d8c894">PR</a></h4><p>The <code>refactor-jasmine-vitest</code> schematic has been marked as stable. This schematic automates the migration from Jasmine test syntax to Vitest, which is now Angular&#8217;s recommended testing framework.</p><p>If you&#8217;ve been holding off on migrating your test suite because the schematic was experimental &#8212; that barrier is gone. You can now run it with confidence:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;9e24ddda-1657-44a1-aabf-b273bc3e8a86&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">ng generate @schematics/angular:refactor-jasmine-vitest</code></pre></div><p>The schematic handles the mechanical parts of the migration: updating imports, converting Jasmine-specific APIs to their Vitest equivalents, and adjusting configuration files. It won&#8217;t rewrite your test logic, but it takes care of the boilerplate changes that make manual migration tedious.</p><p>This stabilization signals that the Angular team considers the Vitest migration path production-ready. If you&#8217;re still on Karma + Jasmine, this is a good time to start planning the switch.</p><p></p><h3>@angular/build</h3><h4>a) Runtime Zone.js detection in Vitest - <a href="https://github.com/angular/angular-cli/commit/414320d02a090b7b36720051569cba7563bf9ac6">PR</a></h4><p>The Vitest unit test runner now supports runtime Zone.js detection. If your project still uses Zone.js, the test runner will detect it automatically &#8212; no manual configuration needed.</p><h4>b) &#9888;&#65039; <code>process.env.PORT</code> takes priority in dev server - <a href="https://github.com/angular/angular-cli/commit/fe720cab64bbc8bcc2db583188e32ad938e63a23">PR</a></h4><p>The <code>ng serve</code> dev server now gives <strong>highest priority</strong> to the <code>PORT</code> environment variable. This overrides both <code>angular.json</code> config and the <code>--port</code> CLI flag &#8212; including the default 4200.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;8c7931ad-f325-49ec-adbd-99c3d95b1abe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript"># This now takes priority over everything
PORT=3000 ng serve
# Even if angular.json says port: 4200 and you pass --port 8080</code></pre></div><p>This is a <strong>breaking change</strong> if you rely on <code>--port</code> or <code>angular.json</code> to override an environment variable. The priority order is now: <code>PORT</code> env var &gt; <code>--port</code> flag &gt; <code>angular.json</code>.</p><h4>c) <code>experimentalPlatform</code> renamed to <code>platform</code> - <a href="https://github.com/angular/angular-cli/commit/af2c7e9444fba81d3b1fd2d37dc4412f8305b5ed">PR</a></h4><p>The <code>experimentalPlatform</code> option in the application builder has been renamed to <code>platform</code>. If you were using this option in your <code>angular.json</code>, update the key name.</p><h4>d) &#9888;&#65039; <code>istanbul-lib-instrument</code> is now an optional peer dependency - <a href="https://github.com/angular/angular-cli/commit/3007f46b7e077227b17d8bb1090edd2f8ff19ae3">PR</a></h4><p>If you&#8217;re using Karma with code coverage, you&#8217;ll need to ensure <code>istanbul-lib-instrument</code> is installed as a direct dependency. The <code>ng update</code> schematic handles this automatically.</p><h4>e) Subresource Integrity (SRI) validation for dynamically loaded modules - <a href="https://github.com/angular/angular-cli/commit/58c7c7a9d80fc6af5cf8b82a6d87f1d3cf3808c6">PR</a></h4><p>This is a security improvement. When you enable <code>subresourceIntegrity: true</code> in your build configuration, Angular already generates integrity hashes for scripts in <code>index.html</code>. But lazy-loaded chunks &#8212; the ones fetched at runtime via dynamic <code>import()</code> &#8212; weren&#8217;t validated.</p><p>Now they are. The build system generates integrity metadata for all lazy chunks, and the runtime validates them before execution. If a CDN or proxy tampers with a lazy-loaded module, the browser will reject it.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;7dd0abed-e7dd-4e76-b49c-b018d6af00e4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "architect": {
    "build": {
      "builder": "@angular/build:application",
      "options": {
        "subresourceIntegrity": true
      }
    }
  }
}</code></pre></div><p>What happens under the hood: the build emits a manifest mapping chunk filenames to their SHA-384 hashes. When Angular&#8217;s router triggers a lazy import, the runtime fetches the chunk and verifies its integrity against the manifest before evaluating it.</p><p>This closes a gap that existed since Angular adopted code splitting. Previously, <code>subresourceIntegrity</code> only protected the initial bundle &#8212; the scripts directly referenced in <code>index.html</code> with <code>integrity</code> attributes. Lazy chunks were unprotected. Now the entire application is covered.</p><p>For apps served behind CDNs or third-party proxies, this is a meaningful security hardening. It ensures that even if an intermediary modifies a JavaScript chunk in transit, the application will refuse to execute it rather than running potentially malicious code.</p><p></p><h3>@angular/ssr</h3><h4>a) &#9888;&#65039; SSR: No more CSR fallback for invalid hosts - <a href="https://github.com/angular/angular-cli/commit/27cd355619aad140dfc221c6bd161f4a981e0f3b">PR</a></h4><p>The Angular SSR server no longer falls back to Client-Side Rendering when a request fails host validation. Requests with unrecognized <code>Host</code> headers now return a <strong>400 Bad Request</strong>. Make sure all valid hosts are configured in the <code>allowedHosts</code> option.</p><p></p><h3>@schematics/angular</h3><h4>a) Strict templates default in generated workspaces - <a href="https://github.com/angular/angular-cli/commit/f98cc82eb0f46986e61b4f94b57dcd36e4eaf215">PR</a></h4><p>New workspaces generated with <code>ng new</code> now rely on the strict template default. This aligns with the migration that adds <code>strictTemplates</code> to existing projects during <code>ng update</code>.</p><h4>b) Karma-to-Vitest migration schematic - <a href="https://github.com/angular/angular-cli/commit/be60a63b7b8fdea26bfd3329ce54d321334db2d2">PR</a></h4><p>A new migration schematic that helps you move from Karma to Vitest. Run it during <code>ng update</code> to automatically update your test configuration.</p><h4>c) Migration to add <code>istanbul-lib-instrument - </code><a href="https://github.com/angular/angular-cli/commit/43505066e2350ca875051bb0f9991da8003c9247">PR</a></h4><p></p><h3>Removed packages</h3><h4>a) &#9888;&#65039; <code>@angular-devkit/architect-cli</code> removed - <a href="https://github.com/angular/angular-cli/commit/1f21e89d99c191642627df6842402644a1bf26ee">PR</a></h4><p>The <code>@angular-devkit/architect-cli</code> package is no longer available. The <code>architect</code> CLI tool has been moved to the <code>@angular-devkit/architect</code> package.</p><h4>b) &#9888;&#65039; Experimental Jest and Web Test Runner builders removed - <a href="https://github.com/angular/angular-cli/commit/b4885b851226709ae4146070122806e14a3d5eb9">PR</a></h4><p>The experimental <code>@angular-devkit/build-angular:jest</code> and <code>@angular-devkit/build-angular:web-test-runner</code> builders have been removed. Vitest is now the recommended test runner going forward.</p><p></p><h2>&#127912; Components &amp; CDK (<code>angular/components</code>)</h2><p></p><h2>&#128230; Migrations &amp; Schematics (cross-cutting)</h2><h4>a) Add <code>strictTemplates</code> to tsconfig during <code>ng update</code> &#8212; <code>angular/angular</code> - <a href="https://github.com/angular/angular/commit/682aaf943fea3d99f9f834b0bad4d165b4b28071">PR</a></h4><p>When you run <code>ng update</code>, Angular now automatically adds <code>strictTemplates: true</code> to your <code>tsconfig.json</code> if it&#8217;s not already there. This has been the recommended setting for a while, and now it&#8217;s enforced during migration.</p><h4>b) Migration schematic for <code>provideHttpClient</code> with XHR &#8212; <code>angular/angular </code>- <a href="https://github.com/angular/angular/commit/3bc095d508653982a48b337afd51bfedbbde1f87">PR</a></h4><p>Since <code>FetchBackend</code> is now the default, a migration schematic is provided to add <code>withXhr()</code> to your <code>provideHttpClient()</code> calls if your app relies on XHR-specific features (like upload progress).</p><h4>c) Migration for <code>ChangeDetectionStrategy.Eager</code> &#8212; <code>angular/angular</code></h4><p>Automatically adds <code>ChangeDetectionStrategy.Eager</code> to components that need it after the default changed to OnPush.</p><h4>d) Migration for <code>CanMatchFn</code> snapshot parameter &#8212; <code>angular/angular </code>- <a href="https://github.com/angular/angular/pull/67452">PR</a></h4><p>The <code>currentSnapshot</code> parameter in <code>CanMatchFn</code> and the <code>canMatch</code> method of the <code>CanMatch</code> interface is now <strong>required</strong>. While the Router already passed this at runtime, existing class implementations must now include the third argument to satisfy the interface. A migration schematic handles this automatically.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;1b16ef73-eefb-4d3a-8ade-1b083f258fea&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#10060; &#8212; third parameter was optional
canMatch(route: Route, segments: UrlSegment[]): boolean {
  return true;
}

// After &#9989; &#8212; currentSnapshot is now required
canMatch(route: Route, segments: UrlSegment[], currentSnapshot: ActivatedRouteSnapshot): boolean {
  return true;
}
</code></pre></div><h4>e) Karma-to-Vitest migration &#8212; <code>angular/angular-cli</code></h4><h4>f) Migration to add <code>istanbul-lib-instrument</code> &#8212; <code>angular/angular-cli</code></h4><h4>g) Auto-Disable nullishCoalescingNotNullable &amp; optionalChainNotNullable on ng update - <a href="https://github.com/angular/angular/pull/68080">PR</a></h4><p>When you run <code>ng update</code> to upgrade to this version, the migration will automatically add suppression rules for the <code>nullishCoalescingNotNullable</code> and <code>optionalChainNotNullable</code> diagnostics to your <code>tsconfig.json</code>.</p><p>This is the companion migration for the breaking compiler change described above. The idea is simple: don&#8217;t block your update with hundreds of new warnings. The migration suppresses them so you can update cleanly, then you can address the warnings incrementally.</p><p>After updating, you&#8217;ll find this in your <code>tsconfig.json</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;2b6fc2c3-e288-4359-b824-d6155666507b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "angularCompilerOptions": {
    "extendedDiagnostics": {
      "checks": {
        "nullishCoalescingNotNullable": "suppress",
        "optionalChainNotNullable": "suppress"
      }
    }
  }
}</code></pre></div><p>Once you&#8217;ve cleaned up the unnecessary <code>?.</code> and <code>??</code> operators in your templates, remove these suppressions to benefit from the stricter type checking going forward.</p><h4>h) Auto-Add <code>strictTemplates</code> to tsconfig During ng update - <a href="https://github.com/angular/angular/commit/682aaf943fea3d99f9f834b0bad4d165b4b28071">PR</a></h4><p>When you run <code>ng update</code> to upgrade to Angular 22, a new migration will automatically add <code>strictTemplates: false</code> to your <code>tsconfig.json</code> under <code>angularCompilerOptions</code> &#8212; but only if it&#8217;s not already set.</p><p>Why <code>false</code>? Because Angular 22 is making <code>strictTemplates: true</code> the default compiler behavior. Projects that haven&#8217;t explicitly opted into strict templates would suddenly get a wave of new type-checking errors in their templates. The migration preserves the previous implicit behavior by making it explicit, so your update doesn&#8217;t break anything.</p><p>The migration is smart about it:</p><ul><li><p>If <code>strictTemplates</code> is already set (to <code>true</code> or <code>false</code>), it leaves it alone</p></li><li><p>If <code>compilerOptions</code> is empty or missing, it skips the file (not a real tsconfig)</p></li><li><p>If <code>angularCompilerOptions</code> doesn&#8217;t exist, it creates the section with <code>strictTemplates: false</code></p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;307678af-4bb4-4093-86a7-8739aee40f14&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">// What the migration adds to your tsconfig.json
{
  "compilerOptions": {
    "target": "es2022"
    // ... your existing options
  },
  "angularCompilerOptions": {
    "strictTemplates": false  // &#8592; added by migration
  }
}</code></pre></div><p>After updating, you can (and should) work toward enabling <code>strictTemplates: true</code> at your own pace. Strict template type checking catches real bugs &#8212; wrong property names, type mismatches in bindings, missing inputs &#8212; that would otherwise only surface at runtime. But the migration ensures you&#8217;re not forced into it during the update itself.</p><h4>i) Model + Output conflict migration - <a href="https://github.com/angular/angular/pull/67349">PR</a></h4><p>When Angular introduced <code>model()</code> signals, it implicitly created a <code>&lt;name&gt;Change</code> output for two-way binding. But some codebases already had explicit <code>output()</code> declarations with that same name &#8212; creating a conflict where the component had duplicate outputs.</p><p>This migration automatically detects the pattern and converts it to <code>input()</code> + <code>linkedSignal()</code>, which preserves the reactive behavior without the naming collision.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;21fabacf-b3d8-40e5-878f-125cc0f25307&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before &#8212; broken duplicate outputs &#10060;
@Component({
  selector: 'app-slider',
  template: '',
})
export class SliderComponent {
  foo = model(0);              // implicitly creates fooChange output
  fooChange = output&lt;number&gt;(); // explicit output &#8212; CONFLICT!
}

// After migration &#9989;
@Component({
  selector: 'app-slider',
  template: '',
})
export class SliderComponent {
  fooInput = input(0, { alias: 'foo' });
  foo = linkedSignal(this.fooInput);
  fooChange = output&lt;number&gt;();
}</code></pre></div><p>The migration handles several cases:</p><ul><li><p>Preserves access modifiers (<code>public</code>, <code>protected</code>, <code>readonly</code>)</p></li><li><p>Handles generic types (<code>model&lt;string&gt;('initial')</code> &#8594; <code>input&lt;string&gt;('initial', {alias: 'bar'})</code>)</p></li><li><p>Adds <code>input</code> and <code>linkedSignal</code> to the import statement automatically</p></li><li><p>Only triggers when there&#8217;s an actual naming conflict &#8212; if your <code>model()</code> doesn&#8217;t have a conflicting explicit output, nothing changes</p></li></ul><p>This runs automatically during <code>ng update</code> to Angular 22.</p><h4>j) Migrate <code>fakeAsync</code> to Vitest fake timers - <a href="http://PR">PR</a></h4><p>The schematic now automatically converts <code>fakeAsync</code>/<code>tick</code>/<code>flush</code> patterns to Vitest&#8217;s native fake timer APIs. Angular&#8217;s <code>fakeAsync</code> was a Zone.js-powered utility &#8212; in a zoneless Vitest world, you use <code>vi.useFakeTimers()</code> and <code>vi.advanceTimersByTime()</code> instead.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;ae194f15-1b66-4033-9a98-52b5723dcdda&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Before (Jasmine + Zone.js) &#10060;
it('should debounce input', fakeAsync(() =&gt; {
  component.onSearch('angular');
  tick(300);
  expect(component.results().length).toBeGreaterThan(0);
}));

// After (Vitest) &#9989;
it('should debounce input', () =&gt; {
  vi.useFakeTimers();
  component.onSearch('angular');
  vi.advanceTimersByTime(300);
  expect(component.results().length).toBeGreaterThan(0);
  vi.useRealTimers();
});</code></pre></div><h4>k) Migrate <code>fakeAsync</code>&#8216;s <code>flush</code> behavior in <code>beforeEach</code> - <a href="https://github.com/angular/angular-cli/commit/d2aa9ede55a3e16b61ce6ae60dba6c8ea8954358">PR</a></h4><p>When <code>fakeAsync</code> with <code>flush()</code> is used inside <code>beforeEach</code> blocks, the schematic now correctly transforms it. This is a common pattern where setup code needs to wait for async initialization to complete before each test.</p><h4>l) Set up fake timers in <code>beforeEach</code> instead of <code>beforeAll</code> - <a href="https://github.com/angular/angular-cli/commit/c9f4081533f6f114846b88a152a9d5dc7363d680">PR</a></h4><p>The schematic ensures fake timers are set up in <code>beforeEach</code> (per-test isolation) rather than <code>beforeAll</code> (shared across tests). This prevents timer state from leaking between tests &#8212; a common source of flaky test suites.</p><h4>m) Update TSConfig globals during migration - <a href="https://github.com/angular/angular-cli/commit/8d0805dd1750cb16af620811dc01b40e46ad030e">PR</a></h4><p>When migrating from Karma to Vitest, the schematic now updates your <code>tsconfig.spec.json</code> to include Vitest&#8217;s global type definitions. This ensures TypeScript recognizes <code>vi</code>, <code>describe</code>, <code>it</code>, <code>expect</code> and other Vitest globals without manual configuration.</p><h4>n) Conditionally install istanbul coverage provider - <a href="https://github.com/angular/angular-cli/commit/b2f7a038b4a321e4e1b0b340cd09425f948c77ad">PR</a></h4><p>During the Karma-to-Vitest migration, the schematic now checks if your project uses code coverage and conditionally installs <code>@vitest/coverage-istanbul</code>. This ensures your existing coverage pipeline (Codecov, SonarQube, etc.) continues working without manual intervention.</p><p></p><h2><strong>&#9888;&#65039; Breaking Changes Summary</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HS3g!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HS3g!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png 424w, https://substackcdn.com/image/fetch/$s_!HS3g!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png 848w, https://substackcdn.com/image/fetch/$s_!HS3g!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png 1272w, https://substackcdn.com/image/fetch/$s_!HS3g!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!HS3g!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png" width="1200" height="3166.1107404936624" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:3955,&quot;width&quot;:1499,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:988932,&quot;alt&quot;:&quot;Angular 22 Breaking Changes Summary&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/193433490?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d527c03-1c65-4ec0-85bb-7f25b41d9fb8_1499x4236.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 22 Breaking Changes Summary" title="Angular 22 Breaking Changes Summary" srcset="https://substackcdn.com/image/fetch/$s_!HS3g!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png 424w, https://substackcdn.com/image/fetch/$s_!HS3g!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png 848w, https://substackcdn.com/image/fetch/$s_!HS3g!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png 1272w, https://substackcdn.com/image/fetch/$s_!HS3g!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce944252-42d4-4494-b37b-1f63e5840a2b_1499x3955.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2><strong>&#9888;&#65039; Deprecations Summary</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tJAm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tJAm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png 424w, https://substackcdn.com/image/fetch/$s_!tJAm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png 848w, https://substackcdn.com/image/fetch/$s_!tJAm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png 1272w, https://substackcdn.com/image/fetch/$s_!tJAm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!tJAm!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png" width="1200" height="657.9075425790754" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:676,&quot;width&quot;:1233,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:105424,&quot;alt&quot;:&quot;Angular 22 Deprecations summary&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/193433490?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 22 Deprecations summary" title="Angular 22 Deprecations summary" srcset="https://substackcdn.com/image/fetch/$s_!tJAm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png 424w, https://substackcdn.com/image/fetch/$s_!tJAm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png 848w, https://substackcdn.com/image/fetch/$s_!tJAm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png 1272w, https://substackcdn.com/image/fetch/$s_!tJAm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8658bd40-686d-4693-b2aa-7052cc2f2f48_1233x676.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Thanks for reading so far &#128591;</strong></h2><p>I&#8217;d like to have your feedback, so please leave a <strong>comment</strong>, <strong>clap</strong> or <strong>follow</strong>. &#128079;</p><p>Spread the Angular love! &#128156;</p><p>If you liked it, <strong>share it</strong> among your community, tech bros and whoever you want! &#128640;&#128101;</p><p>Don&#8217;t forget to follow me and stay in the loop: &#128241;</p><ul><li><p><span>&#128279; </span><strong><a href="https://www.linkedin.com/in/amos-lucian-isaila-34ab78146/">LinkedIn</a></strong></p></li><li><p><span>&#128221; </span><strong><a href="https://medium.com/@amosisaila">Medium</a></strong></p></li><li><p><span>&#127909; </span><strong><a href="https://www.youtube.com/@codigotipado">YouTube</a></strong></p></li><li><p><span>&#128038; </span><strong><a href="https://x.com/amosisaila">Twitter</a></strong></p></li></ul><p>Thanks for being part of this Angular adventure! &#128075;&#128513;</p>]]></content:encoded></item><item><title><![CDATA[injectAsync: Angular's Native Solution for Lazy-Loading Services]]></title><description><![CDATA[Angular 22 ships injectAsync &#8212; a first-party helper that lets you lazily load services through the DI system.]]></description><link>https://www.codigotipado.com/p/injectasync-angulars-native-solution</link><guid isPermaLink="false">https://www.codigotipado.com/p/injectasync-angulars-native-solution</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Wed, 06 May 2026 05:38:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!T9k1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T9k1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T9k1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!T9k1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!T9k1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!T9k1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T9k1!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/af61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:489803,&quot;alt&quot;:&quot;Angular 22: injectAsync:  Lazy-Loading Services&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/196620785?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 22: injectAsync:  Lazy-Loading Services" title="Angular 22: injectAsync:  Lazy-Loading Services" srcset="https://substackcdn.com/image/fetch/$s_!T9k1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!T9k1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!T9k1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!T9k1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faf61d742-fc91-4eb2-92d4-d0019c623769_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you&#8217;ve used <code>ngxtension/inject-lazy</code>, this will feel familiar. But the API is different, and the trade-offs matter.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>The Problem</h2><p>Bundle size. You have a <code>ChartService</code> that pulls in D3, a <code>PdfService</code> that imports pdfjs, or an <code>AnalyticsService</code> that loads a heavy SDK. These services aren&#8217;t needed on initial render &#8212; maybe they&#8217;re triggered by a button click, a route guard, or a conditional feature flag.</p><p>With <code>providedIn: 'root'</code>, the service class and its dependencies end up in the main bundle regardless of whether the user ever triggers the code path that uses them.</p><p>Lazy routes solve this for route-level code splitting. But what about services used <em>within</em> a component that&#8217;s already loaded?</p><h2>Before <code>injectAsync</code>: The Manual Approach</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;2f285e19-ebd0-44d6-8f35-105f3869ad03&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">@Component({
  selector: 'app-report',
  template: `&lt;button (click)="generate()"&gt;Generate PDF&lt;/button&gt;`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReportComponent {
  private _injector = inject(EnvironmentInjector);

  async generate(): Promise&lt;void&gt; {
    const { PdfService } = await import('./pdf.service');
    const pdfService = this._injector.get(PdfService);
    pdfService.createReport();
  }
}</code></pre></div>
      <p>
          <a href="https://www.codigotipado.com/p/injectasync-angulars-native-solution">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[You Can Now Control What Binds to Your Component Inputs in Angular 22]]></title><description><![CDATA[I've been using withComponentInputBinding() since Angular 16 and honestly, it's one of my favorite router features. No more injecting ActivatedRoute, no more subscribing to params.]]></description><link>https://www.codigotipado.com/p/you-can-now-control-what-binds-to</link><guid isPermaLink="false">https://www.codigotipado.com/p/you-can-now-control-what-binds-to</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Mon, 06 Apr 2026 21:40:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!feJ8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!feJ8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!feJ8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png 424w, https://substackcdn.com/image/fetch/$s_!feJ8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png 848w, https://substackcdn.com/image/fetch/$s_!feJ8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png 1272w, https://substackcdn.com/image/fetch/$s_!feJ8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!feJ8!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png" width="1200" height="685.7142857142857" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:768,&quot;width&quot;:1344,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:1907019,&quot;alt&quot;:&quot;Angular 22: withComponentInputBinding&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/193396918?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 22: withComponentInputBinding" title="Angular 22: withComponentInputBinding" srcset="https://substackcdn.com/image/fetch/$s_!feJ8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png 424w, https://substackcdn.com/image/fetch/$s_!feJ8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png 848w, https://substackcdn.com/image/fetch/$s_!feJ8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png 1272w, https://substackcdn.com/image/fetch/$s_!feJ8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff14f6c86-facb-4895-8f9c-ed831252cf04_1344x768.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>The short version</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;5fcbd6b5-aa81-4423-8ec6-790980f439b7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">provideRouter(routes, withComponentInputBinding({ queryParams: false }))</code></pre></div><p>That&#8217;s it. Query params no longer bind to your component inputs. Path params and route data? Still work fine.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>So what was the problem?</h2><p>Let me show you. Say you have this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;ad22c3a7-fd3a-494f-b41b-66fc5c190c30&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">// Route: { path: 'user/:id', component: UserComponent, data: { role: 'admin' } }
// URL: /user/42?tab=settings

@Component({ ... })
export class UserComponent {
  id = input&lt;string&gt;();       // &#8592; from :id
  tab = input&lt;string&gt;();      // &#8592; from ?tab=
  role = input&lt;string&gt;();     // &#8592; from route data
}</code></pre></div><p>All three sources &#8212; path params, query params, route data &#8212; bind to your inputs automatically. Sounds great until you realize that <strong>query params are completely user-controlled</strong>. Anyone can type whatever they want in the URL bar.</p><p>Picture this:</p>
      <p>
          <a href="https://www.codigotipado.com/p/you-can-now-control-what-binds-to">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular 22: Debounced Signal]]></title><description><![CDATA[How Angular turned signal debouncing into a first-class primitive &#8212; and why it returns a Resource.]]></description><link>https://www.codigotipado.com/p/angular-22-debounced-signal</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-22-debounced-signal</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Mon, 30 Mar 2026 21:07:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!queC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!queC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!queC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!queC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!queC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!queC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!queC!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:937209,&quot;alt&quot;:&quot;Angular 22: Debounced&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/191804884?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 22: Debounced" title="Angular 22: Debounced" srcset="https://substackcdn.com/image/fetch/$s_!queC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!queC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!queC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!queC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F56551152-5bb8-4258-8e23-e7e92da1b75c_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Before you start diving into this new topic you can review the <a href="https://www.codigotipado.com/p/angular-19-resource-api-for-async">full guide of resource() that was introduced in Angular 19</a>.</p><h2>TL;DR</h2><p>Angular added a <code>debounced()</code> function that takes a signal and a wait time, and returns a <strong>Resource</strong>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;8502c48b-10d4-49ad-a9e5-8cd3bed622e6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">signal &#8594; debounced(signal, 500) &#8594; Resource&lt;T&gt;</code></pre></div><p>The resource&#8217;s <code>value()</code> holds the debounced value. Its <code>status</code> tells you whether the value is settled (<code>'resolved'</code>), still waiting (<code>'loading'</code>), or if the source threw (<code>'error'</code>). No RxJS, no <code>setTimeout</code> wrappers, no custom hooks. Just signals in, Resource out.</p><h2>The Problem</h2><p>You have a search input. The user types &#8220;Tokyo&#8221;. Without debouncing, you fire 5 HTTP requests: one per keystroke. That&#8217;s wasteful, potentially slow, and can cause race conditions if responses arrive out of order.</p><p><strong>The RxJS way:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;430be340-e35b-4da3-89a8-8b5f6e5dde3f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">searchControl.valueChanges.pipe(
  debounceTime(500),
  distinctUntilChanged(),
  switchMap(query =&gt; this.http.get(`/api/search?q=${query}`))
).subscribe(results =&gt; { ... });</code></pre></div><p><strong>The manual way:</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;5e1bb7d4-2c3c-45e3-8f87-2e3ad51a3a09&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">let timer: ReturnType&lt;typeof setTimeout&gt;;
function onInput(value: string) {
  clearTimeout(timer);
  timer = setTimeout(() =&gt; {
    // now do something with value
  }, 500);
}</code></pre></div><p>Both work. Neither integrates with signals. You end up bridging between observables or callbacks and signals, losing reactivity along the way.</p><p>The real gap: there was no signal-native way to debounce a value and get a reactive, composable result back.</p><h2>The Solution: <code>debounced()</code></h2><h3>The API</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;c3dbf6d7-2d4f-438e-8a09-701469c06e99&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { debounced } from '@angular/core';

const search = signal('');
const debouncedSearch = debounced(() =&gt; search(), 500);</code></pre></div><p><code>debouncedSearch</code> is a <code>Resource&lt;string&gt;</code>. It has everything you&#8217;d expect:</p><ul><li><p><code>debouncedSearch.value()</code> &#8212; the debounced value (updates only after 500ms of silence)</p></li><li><p><code>debouncedSearch.status()</code> &#8212; <code>'resolved'</code> when settled, <code>'loading'</code> while waiting, <code>'error'</code> if the source threw</p></li><li><p><code>debouncedSearch.isLoading()</code> &#8212; <code>true</code> while a value is pending</p></li><li><p><code>debouncedSearch.error()</code> &#8212; the error, if the source signal threw</p></li></ul><h3>Full Signature</h3>
      <p>
          <a href="https://www.codigotipado.com/p/angular-22-debounced-signal">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular 21.2: Resource Composition via Snapshots]]></title><description><![CDATA[A deep dive into Angular's new snapshot-based pattern for transforming and composing resources without losing their interface.]]></description><link>https://www.codigotipado.com/p/angular-212-resource-composition</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-212-resource-composition</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Sun, 22 Mar 2026 21:31:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cJhZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cJhZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cJhZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png 424w, https://substackcdn.com/image/fetch/$s_!cJhZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png 848w, https://substackcdn.com/image/fetch/$s_!cJhZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png 1272w, https://substackcdn.com/image/fetch/$s_!cJhZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cJhZ!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png" width="1200" height="674.0625" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:719,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:718675,&quot;alt&quot;:&quot;Angular 21.2: Resource Snapshost&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/191735733?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 21.2: Resource Snapshost" title="Angular 21.2: Resource Snapshost" srcset="https://substackcdn.com/image/fetch/$s_!cJhZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png 424w, https://substackcdn.com/image/fetch/$s_!cJhZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png 848w, https://substackcdn.com/image/fetch/$s_!cJhZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png 1272w, https://substackcdn.com/image/fetch/$s_!cJhZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F656702e6-2002-4a1a-bfe1-11d1ee072ac9_1280x719.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Before you start diving into this new topic you can review the <a href="https://www.codigotipado.com/p/angular-19-resource-api-for-async">full guide of resource() that was introduced in Angular 19</a>.</p><h2>TL;DR</h2><p>Angular added three things that let you take a resource, modify its behavior, and get back a real resource.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;2676756c-cf8b-4922-9f3f-f302192465b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Resource  &#8594;  .snapshot  &#8594;  transform it  &#8594;  resourceFromSnapshots()  &#8594;  Resource</code></pre></div><p>The killer use case? <strong>Keep showing old data while new data loads</strong> &#8212; no more blank screens when switching between items. And it&#8217;s a generic utility you write once and reuse everywhere.</p><p><strong>PR</strong>: <a href="https://github.com/angular/angular/commit/1ba9b7ac5001b315cc9df78c518964dbf479d647">feat(core): resource composition via snapshots</a><br><strong>Authors</strong>: Alex Rickabaugh &amp; Jessica Janiuk<br><strong>Status</strong>: Experimental &#183; <strong>Angular 21.2+</strong></p><p><strong><a href="https://github.com/amosISA/angular-signal-forms/tree/angular-21.2">Code Repository Example in this article can be found here</a>.</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>The Problem (in Plain English)</h2><p>Imagine you have a weather app. The user is looking at London&#8217;s weather. They click &#8220;Tokyo.&#8221; What happens?</p><p><strong>Before this PR:</strong></p><ol><li><p>The resource enters &#8220;loading&#8221; state</p></li><li><p><code>value()</code> immediately becomes <code>undefined</code></p></li><li><p>The weather card <strong>disappears</strong> &#8212; blank screen, skeleton loader</p></li><li><p>Tokyo&#8217;s data arrives, card re-appears</p></li></ol><p>That flash of empty content feels broken. Users hate it.</p><p>You might think: <em>&#8220;I&#8217;ll just keep the old value around in a separate signal.&#8221;</em> Sure, but then you lose the <code>Resource</code> interface. Your workaround doesn&#8217;t have <code>.isLoading()</code>, <code>.hasValue()</code>, <code>.status()</code>. It&#8217;s a one-off hack that doesn&#8217;t compose.</p><p><strong>The real problem:</strong> there was no way to take a <code>Resource</code>, tweak its behavior, and get back something that&#8217;s still a <code>Resource</code>.</p>
      <p>
          <a href="https://www.codigotipado.com/p/angular-212-resource-composition">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Migrating to Signal Forms: The SignalFormControl Bridge in Angular 21.2]]></title><description><![CDATA[You don't have to rewrite all your forms at once. Angular 21.2 ships two interop tools that let you migrate one field &#8212; or one form &#8212; at a time.]]></description><link>https://www.codigotipado.com/p/migrating-to-signal-forms-the-signalformcontrol</link><guid isPermaLink="false">https://www.codigotipado.com/p/migrating-to-signal-forms-the-signalformcontrol</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Sat, 14 Mar 2026 22:14:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!O2M7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!O2M7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!O2M7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!O2M7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!O2M7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!O2M7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!O2M7!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:1206915,&quot;alt&quot;:&quot;Angular 21.2.0: SignalFormControl&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/190912876?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 21.2.0: SignalFormControl" title="Angular 21.2.0: SignalFormControl" srcset="https://substackcdn.com/image/fetch/$s_!O2M7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!O2M7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!O2M7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!O2M7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9be8e0a9-02e1-43cc-b46c-eb1657b11e28_1920x1080.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>The Problem Nobody Talks About</strong></h2><p>Every time Angular introduces a new API, the community gets excited about greenfield projects. &#8220;Look how clean this is!&#8221; And it <em>is</em> clean. Signal forms are a genuine improvement over reactive forms &#8212; less boilerplate, better type safety, signals instead of observables.</p><p>But here&#8217;s the thing: most of us aren&#8217;t building new apps from scratch. We&#8217;re maintaining applications with dozens &#8212; sometimes hundreds &#8212; of reactive forms. Forms that work. Forms that are tested. Forms that have edge cases baked into them over years of production use.</p><p>The question isn&#8217;t &#8220;are signal forms better?&#8221; &#8212; they are. The question is: <strong>how do you get there without rewriting everything?</strong></p><p>Angular 21.2 answers this with two interop tools:</p><ol><li><p><code>SignalFormControl</code> &#8212; A signal-based control that extends <code>AbstractControl</code>, so it plugs directly into existing <code>FormGroup</code> and <code>FormArray</code> structures</p></li><li><p><code>compatForm</code> &#8212; A version of <code>form()</code> that accepts reactive <code>FormControl</code> instances as part of the data model</p></li></ol><p>Both are exported from <code>@angular/forms/signals/compat</code>. Both are experimental. And both solve different migration scenarios.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2><strong>Understanding the Two Migration Paths</strong></h2><p>Before diving into code, it&#8217;s worth understanding when to use each approach.</p><p><code>SignalFormControl</code> is for when you want to <strong>migrate individual fields</strong> inside an existing reactive form. You replace one <code>FormControl</code> with a <code>SignalFormControl</code>, and the parent <code>FormGroup</code> doesn&#8217;t even notice &#8212; it just sees an <code>AbstractControl</code>.</p><p><code>compatForm</code> is for when you want to <strong>migrate the form itself</strong> to signal forms, but some fields are still <code>FormControl</code> instances that you&#8217;re not ready to convert yet.</p><p>Think of it this way:</p><ul><li><p>I want to add signal validation to one field in my reactive form &#8594; <code>SignalFormControl</code>Reactive form &#8594; signal field</p></li><li><p>I want to use <code>form()</code> but keep some existing <code>FormControl</code>s &#8594; <code>compatForm</code>Signal form &#8594; reactive fields</p></li></ul><p>Most teams will start with <code>SignalFormControl</code> because it&#8217;s the least disruptive. You change one field, ship it, observe, then do the next one.</p><h2><code>SignalFormControl</code><strong>: Your First Migration Step</strong></h2><h3><strong>What It Is</strong></h3><p><code>SignalFormControl</code> extends <code>AbstractControl</code> &#8212; the same base class that <code>FormControl</code>, <code>FormGroup</code>, and <code>FormArray</code> extend. This means it&#8217;s a drop-in replacement anywhere Angular&#8217;s reactive forms infrastructure expects a control.</p><p>Internally, it&#8217;s backed by a <code>WritableSignal&lt;T&gt;</code> and a signal forms <code>FieldTree</code>. It translates between the two worlds: when the reactive form reads <code>.value</code>, it gets the signal&#8217;s current value. When signal forms validation runs, the results are exposed through the reactive <code>.errors</code> getter.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;typescript&quot;,&quot;nodeId&quot;:&quot;6bc72854-4132-46d2-84c1-805d70a23046&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-typescript">import { SignalFormControl } from '@angular/forms/signals/compat';</code></pre></div>
      <p>
          <a href="https://www.codigotipado.com/p/migrating-to-signal-forms-the-signalformcontrol">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular 21.2: What's new]]></title><description><![CDATA[Angular 21.2.0 introduces TypeScript 6 support, exhaustive @switch checks, instanceof in templates, massive signal forms improvements, nested animations and a bunch of DX improvements.]]></description><link>https://www.codigotipado.com/p/angular-212-whats-new</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-212-whats-new</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Sun, 08 Mar 2026 22:41:51 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!NRqH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NRqH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NRqH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!NRqH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!NRqH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!NRqH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NRqH!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:836942,&quot;alt&quot;:&quot;Angular 21.2 What's new&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/190231022?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 21.2 What's new" title="Angular 21.2 What's new" srcset="https://substackcdn.com/image/fetch/$s_!NRqH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!NRqH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!NRqH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!NRqH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc3a0eab3-c75f-426b-8da9-e1f297c4caf8_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2><strong>1. @angular/animations</strong></h2><h2><strong>2. @angular/build</strong></h2><h3><strong>a) Headless Option for Unit-Test Builder - <a href="https://github.com/angular/angular-cli/commit/ece30f2359c2dc794b0c9272447f623a121e88b0">PR</a></strong></h3><p>The unit-test builder now supports a <code>headless</code> option for browser-based tests. You can toggle between headless and headed mode without modifying your Vitest configuration.</p><pre><code><code>ng test --headless=false  # Run with visible browser for debugging
ng test                   # Default: headless</code></code></pre><h3><strong>b) Vitest Browser with Playwright OS Theme - <a href="https://github.com/angular/angular-cli/commit/cad7a7c0ff3778f04820a99ad0aa9d74f1067fd5">PR</a></strong></h3><p>When running Vitest browser tests with Playwright, the browser now respects your operating system&#8217;s theme (light/dark mode). This means your tests run in an environment that matches your system preferences, which can be relevant for testing theme-dependent UI behavior.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2><strong>3. @angular/common</strong></h2><h3><strong>a) Outlet Injector Option for </strong><code>ngTemplateOutlet</code><strong> - <a href="https://github.com/angular/angular/pull/55389">PR</a></strong></h3><p>To understand this feature, you first need to understand a quirk of how <code>ngTemplateOutlet</code> works with Angular&#8217;s dependency injection.</p><p><strong>The problem: templates resolve services from where they&#8217;re </strong><em><strong>defined</strong></em><strong>, not where they&#8217;re </strong><em><strong>rendered</strong></em><strong>.</strong></p><p>Imagine you define an <code>&lt;ng-template&gt;</code> at the root of your component. Inside that template, you try to <code>inject()</code> a service. Angular will look for that service starting from the <em>definition site</em> of the template (where the <code>&lt;ng-template&gt;</code> tag lives in the source code), not from where the template is actually stamped into the DOM via <code>*ngTemplateOutlet</code>.</p><p>Here&#8217;s a concrete example to make this tangible:</p>
      <p>
          <a href="https://www.codigotipado.com/p/angular-212-whats-new">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular 21.1: What's new]]></title><description><![CDATA[This release enhances the new signal forms API with programmatic focus control, adds fall-through support to @switch control flow, and introduces spread operators in templates.]]></description><link>https://www.codigotipado.com/p/angular-211-whats-new</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-211-whats-new</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Thu, 22 Jan 2026 05:28:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!RyIO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RyIO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RyIO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!RyIO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!RyIO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!RyIO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RyIO!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:1319337,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/180478347?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RyIO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!RyIO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!RyIO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!RyIO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a38b317-24a5-46bf-9611-cab929b9466e_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>1. @angular/common</strong></h2><h3>a) Custom Transformations for Cloudflare and Cloudinary Image Loaders - <a href="https://github.com/angular/angular/commit/d8790972bea4c59a208219dd36d158b5d7e4fdde">PR</a></h3><p>Adds support for custom transformations to Cloudinary and Cloudflare image loaders via a <code>transform</code> parameter.</p><p><a href="https://angular.dev/guide/image-optimization#configuring-an-image-loader-for-ngoptimizedimage">You can learn more about How to Configure an image loader for NgOptimizedImage</a>.</p><h3>b) Custom Transformations for ImageKit and Imgix Loaders - <a href="https://github.com/angular/angular/commit/a6b8cb68afaded6999ee68f495512be1a9932ae4">PR</a></h3><p>Similar custom transformation support extends to ImageKit and Imgix, two other popular image CDN providers.</p><p><a href="https://angular.dev/guide/image-optimization#configuring-an-image-loader-for-ngoptimizedimage">You can learn more about How to Configure an image loader for NgOptimizedImage</a>.</p>
      <p>
          <a href="https://www.codigotipado.com/p/angular-211-whats-new">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Making Sense of Metadata in Angular Signal Forms]]></title><description><![CDATA[A practical guide using a real weather chatbot that validates cities against an API]]></description><link>https://www.codigotipado.com/p/making-sense-of-metadata-in-angular</link><guid isPermaLink="false">https://www.codigotipado.com/p/making-sense-of-metadata-in-angular</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Mon, 15 Dec 2025 07:11:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!yt0Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;m going to walk you through metadata in Angular Signal Forms using a real project I built&#8212;a weather chatbot that checks if cities actually exist before letting you submit the form. No abstract examples, just working code solving actual problems.</p><p>Here&#8217;s what makes this interesting: the form doesn&#8217;t just validate that you typed something. It calls an API to verify the city exists in a weather database, manages the loading state while checking, caches results so you don&#8217;t hammer the server, and gives instant feedback. That&#8217;s where metadata becomes essential.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yt0Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yt0Y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!yt0Y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!yt0Y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!yt0Y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yt0Y!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/09bd6998-b151-482a-966f-2482517567e3_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:1021922,&quot;alt&quot;:&quot;Angular Signal Forms: Metadata&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/181649579?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular Signal Forms: Metadata" title="Angular Signal Forms: Metadata" srcset="https://substackcdn.com/image/fetch/$s_!yt0Y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!yt0Y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!yt0Y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!yt0Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09bd6998-b151-482a-966f-2482517567e3_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>The Core Problem</h2><p>Traditional validation answers simple questions: Is the field empty? Does it match this pattern? But modern forms need smarter behavior. When someone types &#8220;Barcelona, Spain,&#8221; you want to know:</p><ul><li><p>Does this city exist in your weather database?</p></li><li><p>Are we currently checking with the server?</p></li><li><p>Did we already validate this exact combination?</p></li><li><p>Should we show a spinner?</p></li></ul><p>Your form data is just <code>{ city: &#8220;Barcelona&#8221;, country: &#8220;Spain&#8221; }</code>. Everything else&#8212;the validation state, the API response, the loading indicators&#8212;that&#8217;s metadata. It&#8217;s the difference between what the user entered and what you need to know about what they entered.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Starting With Basic Validation</h2><p>Let&#8217;s look at the foundation. This is pure validation, no async behavior yet:</p><pre><code>const cityNameSchema = schema&lt;string&gt;((path) =&gt; {
  required(path, { message: &#8216;City is required&#8217; });
  minLength(path, 2, { message: &#8216;City must be at least 2 characters&#8217; });
  maxLength(path, 50, { message: &#8216;City name is too long&#8217; });
});

export const locationSchema = schema&lt;WeatherLocation&gt;((path) =&gt; {
  apply(path.city, cityNameSchema);
  apply(path.country, countryNameSchema);
});</code></pre><p>This checks structure and format. But it doesn&#8217;t verify if Barcelona is real or if someone made it up. For that, you need to talk to an API.</p><h2>Adding Real Validation</h2><p>Here&#8217;s where it gets interesting. We&#8217;re going to validate cities against a weather API as the user types:</p>
      <p>
          <a href="https://www.codigotipado.com/p/making-sense-of-metadata-in-angular">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular 21.0.1: The Missing "Style Link" in Signal Forms 🔗]]></title><description><![CDATA[If you've been experimenting with the new Signal Forms in Angular 21, you might have noticed something missing. You set up your schemas, you wired up your signals, but the DOM is clean.]]></description><link>https://www.codigotipado.com/p/angular-211-the-missing-style-link</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-211-the-missing-style-link</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Tue, 02 Dec 2025 05:49:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!RVdM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RVdM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RVdM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!RVdM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!RVdM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!RVdM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RVdM!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:688922,&quot;alt&quot;:&quot;Angular 21.0.1 Signal Forms - Field Directive Classes with DI&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/180473602?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular 21.0.1 Signal Forms - Field Directive Classes with DI" title="Angular 21.0.1 Signal Forms - Field Directive Classes with DI" srcset="https://substackcdn.com/image/fetch/$s_!RVdM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!RVdM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!RVdM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!RVdM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8168c31b-2a24-4bdd-a1af-c91990baf026_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you&#8217;ve been experimenting with the new <strong>Signal Forms</strong> in Angular 21, you might have noticed something missing. You set up your schemas, you wired up your signals, but when you looked at the DOM... it was clean.</p><p>Too clean.</p><p>In the old <strong>ReactiveForms</strong> world, Angular automatically slapped classes like <code>.ng-invalid</code>, <code>.ng-dirty</code>, and <code>.ng-touched</code> onto your inputs. CSS frameworks (like Bootstrap or Material) relied on these hooks to turn borders red or show error icons. With Signal Forms, that automatic behavior was gone&#8212;until now.</p><p>With the latest merge into core (<a href="https://github.com/angular/angular/commit/7d5c7cf99aa5c6490f8bea950b04bd56073582a1">Commit </a><code>7d5c7cf</code>), Angular 21.1 introduces <code>provideSignalFormsConfig</code>. This acts as a bridge between your pure logic signal state and your UI styling.</p><p>Let&#8217;s look at how this changes the game for our Weather Chatbot, specifically how we can use it to drive a <strong>&#8220;Tailwind-native&#8221;</strong> form experience without cluttering our templates.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>The &#8220;Lazy&#8221; Way: Back to Basics</h2><p>If you just want the old behavior back because you have existing CSS that targets <code>.ng-invalid</code>, the Angular team provided a pre-baked constant.</p><p>In your <code>app.config.ts</code>:</p><pre><code><code>import { ApplicationConfig } from &#8216;@angular/core&#8217;;
import { provideSignalFormsConfig } from &#8216;@angular/forms/signals&#8217;;
import { NG_STATUS_CLASSES } from &#8216;@angular/forms/signals/compat&#8217;;

export const appConfig: ApplicationConfig = {
  providers: [
    // &#129668; Magic: Automatically adds ng-valid, ng-invalid, ng-dirty, etc.
    provideSignalFormsConfig({
      classes: NG_STATUS_CLASSES
    })
  ]
};</code></code></pre><p>But we are writing modern Angular. We can do better than legacy compatibility.</p><h2>The &#8220;Pro&#8221; Way: Semantic Configuration</h2><p>This new API allows us to map <strong>any CSS class</strong> to a <strong>predicate function</strong> that reads the field state. This is incredibly powerful.</p>
      <p>
          <a href="https://www.codigotipado.com/p/angular-211-the-missing-style-link">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Angular 21: What's new]]></title><description><![CDATA[The most significant shift is the zoneless-by-default approach. The future is reactive, performant, and decidedly signal-driven&#8212;and Angular is leading the way.]]></description><link>https://www.codigotipado.com/p/angular-21-whats-new</link><guid isPermaLink="false">https://www.codigotipado.com/p/angular-21-whats-new</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Thu, 20 Nov 2025 20:15:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!s4ZX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!s4ZX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s4ZX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png 424w, https://substackcdn.com/image/fetch/$s_!s4ZX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png 848w, https://substackcdn.com/image/fetch/$s_!s4ZX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png 1272w, https://substackcdn.com/image/fetch/$s_!s4ZX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s4ZX!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/de77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2116056,&quot;alt&quot;:&quot;Angular 21: What's new&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/171535173?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="Angular 21: What's new" title="Angular 21: What's new" srcset="https://substackcdn.com/image/fetch/$s_!s4ZX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png 424w, https://substackcdn.com/image/fetch/$s_!s4ZX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png 848w, https://substackcdn.com/image/fetch/$s_!s4ZX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png 1272w, https://substackcdn.com/image/fetch/$s_!s4ZX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde77d65f-d362-4dec-883e-6d48be2c76b7_1600x900.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>1. @angular/animations</strong></h2><h2><strong>2. @angular/common</strong></h2><h3>a) Zero-Configuration HTTP Services - <a href="https://github.com/angular/angular/pull/56212">PR</a></h3><p>HTTP services are now automatically provided in the <strong>root injector</strong>, eliminating the need for explicit <code>provideHttpClient()</code>.</p><pre><code>import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';

// Before
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient() // &#10060; Required for basic HTTP functionality
  ]
});

// After
bootstrapApplication(AppComponent, {
  providers: [
    // &#9989; HTTP services automatically available
  ]
});</code></pre><p><strong>Advanced Configuration:</strong> For custom interceptors, client configuration, or specific features, continue using <code>provideHttpClient()</code>:</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/p/angular-21-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Angular: From Zero to Expert! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/p/angular-21-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.codigotipado.com/p/angular-21-whats-new?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><pre><code>bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor]),
      withFetch() // Custom features require explicit provider
    )
  ]
});</code></pre><h3>b) <strong>&#9888;&#65039; </strong>NgComponentOutlet NgModuleFactory Removal - <a href="https://github.com/angular/angular/pull/62838">PR</a></h3><p>The <code>ngModuleFactory</code> input of <code>NgComponentOutlet</code> was a legacy feature that relied on the deprecated <code>NgModuleFactory</code> class, creating unnecessary complexity and maintenance overhead.</p><p>Now <code>ngModuleFactory</code> input from <code>NgComponentOutlet</code> is removed, using only the modern <code>NgModule</code> approach.</p>
      <p>
          <a href="https://www.codigotipado.com/p/angular-21-whats-new">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Mastering the New debounce() API in Angular 21 Signal Forms]]></title><description><![CDATA[In this comprehensive guide, we'll explore the debounce() API through a real-world weather chatbot application, demonstrating how this feature transforms form performance and user experience.]]></description><link>https://www.codigotipado.com/p/mastering-the-new-debounce-api-in</link><guid isPermaLink="false">https://www.codigotipado.com/p/mastering-the-new-debounce-api-in</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Wed, 19 Nov 2025 05:26:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!d1H-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!d1H-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d1H-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!d1H-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!d1H-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!d1H-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d1H-!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:1216738,&quot;alt&quot;:&quot;Angular Signal Forms Debounce&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/179322193?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Angular Signal Forms Debounce" title="Angular Signal Forms Debounce" srcset="https://substackcdn.com/image/fetch/$s_!d1H-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!d1H-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!d1H-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!d1H-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe35a7332-7925-4fe6-88b2-e934f5ed0cfd_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>The Problem We&#8217;ve All Been Ignoring</h2><p>Here&#8217;s something embarrassing: I had a search field in one of my apps that was hitting our ElasticSearch cluster <strong>on every single keystroke</strong>. A user typing &#8220;San Francisco&#8221; would trigger 13 API calls. Multiply that by a few hundred concurrent users, and our AWS bill was... let&#8217;s just say &#8220;uncomfortable.&#8221;</p><p>The old solution? RxJS operators like <code>debounceTime()</code> scattered throughout components, manual subscription management, and crossing your fingers that you didn&#8217;t create a memory leak. It worked, but it felt like duct tape on a leaky pipe.</p><p>Signal Forms&#8217; new <code>debounce()</code> rule takes a completely different approach. Instead of managing debouncing at the component level, it&#8217;s built directly into the form&#8217;s schema definition. The form itself knows when to synchronize user input with your data model.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.codigotipado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>
      <p>
          <a href="https://www.codigotipado.com/p/mastering-the-new-debounce-api-in">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Building Your Own MCP Server: Angular Best Practices Refactoring Tool - Part 2]]></title><description><![CDATA[In Part 1, we explored the theory behind Model Context Protocol, now it's time to get our hands dirty.]]></description><link>https://www.codigotipado.com/p/building-your-own-mcp-server-angular</link><guid isPermaLink="false">https://www.codigotipado.com/p/building-your-own-mcp-server-angular</guid><dc:creator><![CDATA[Amos Isaila]]></dc:creator><pubDate>Sat, 01 Nov 2025 15:42:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!q8a7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!q8a7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!q8a7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!q8a7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!q8a7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!q8a7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!q8a7!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:640327,&quot;alt&quot;:&quot;Build you Own MCP Server: Part 2&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.codigotipado.com/i/176709818?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-large" alt="Build you Own MCP Server: Part 2" title="Build you Own MCP Server: Part 2" srcset="https://substackcdn.com/image/fetch/$s_!q8a7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png 424w, https://substackcdn.com/image/fetch/$s_!q8a7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png 848w, https://substackcdn.com/image/fetch/$s_!q8a7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png 1272w, https://substackcdn.com/image/fetch/$s_!q8a7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc1ae624-c88d-47bd-903b-568d348f78ac_1280x720.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But here&#8217;s where we&#8217;re going to do something different from typical tutorials. Instead of building a generic weather API or todo list MCP server, we&#8217;re creating something immediately valuable for Angular developers: <strong>an MCP server with a powerful refactoring tool that enforces modern Angular and TypeScript best practices</strong>.</p>
      <p>
          <a href="https://www.codigotipado.com/p/building-your-own-mcp-server-angular">
              Read more
          </a>
      </p>
   ]]></content:encoded></item></channel></rss>