<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Amos Isaila</title>
    <description>The latest articles on Forem by Amos Isaila (@amosisaila).</description>
    <link>https://forem.com/amosisaila</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F18455%2Ff8636029-ac79-47a3-a8e8-3a2c62c796f5.png</url>
      <title>Forem: Amos Isaila</title>
      <link>https://forem.com/amosisaila</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/amosisaila"/>
    <language>en</language>
    <item>
      <title>Angular 21.1: What’s new</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Thu, 22 Jan 2026 05:46:58 +0000</pubDate>
      <link>https://forem.com/amosisaila/angular-211-whats-new-21ok</link>
      <guid>https://forem.com/amosisaila/angular-211-whats-new-21ok</guid>
      <description>&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/aangular-21-1-whats-new-ccdf804c3442?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frq8jjha1s9ocuv0y32vd.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This release enhances the new signal forms API with programmatic focus control, adds fall-through support to &lt;a class="mentioned-user" href="https://dev.to/switch"&gt;@switch&lt;/a&gt; control flow, and…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/aangular-21-1-whats-new-ccdf804c3442?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>climcp</category>
      <category>templatespread</category>
      <category>signalformsfocus</category>
      <category>signalforms</category>
    </item>
    <item>
      <title>Making Sense of Metadata in Angular Signal Forms</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Mon, 15 Dec 2025 07:31:24 +0000</pubDate>
      <link>https://forem.com/amosisaila/making-sense-of-metadata-in-angular-signal-forms-47dm</link>
      <guid>https://forem.com/amosisaila/making-sense-of-metadata-in-angular-signal-forms-47dm</guid>
      <description>&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/making-sense-of-metadata-in-angular-signal-forms-03c1ca969ee3?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fozdi47yxl7al83ejfw3h.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A practical guide using a real weather chatbot that validates cities against an API&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/making-sense-of-metadata-in-angular-signal-forms-03c1ca969ee3?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>signal</category>
      <category>angular</category>
      <category>angularsignalforms</category>
      <category>angualar21</category>
    </item>
    <item>
      <title>Angular 21.0.1: The Missing “Style Link” in Signal Forms</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Tue, 02 Dec 2025 06:06:39 +0000</pubDate>
      <link>https://forem.com/amosisaila/angular-2101-the-missing-style-link-in-signal-forms-1je2</link>
      <guid>https://forem.com/amosisaila/angular-2101-the-missing-style-link-in-signal-forms-1je2</guid>
      <description>&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/angular-21-0-1-the-missing-style-link-in-signal-forms-bb8571e90f61?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn7inuczme2g0tdzdgjgi.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’ve been experimenting with the new Signal Forms in Angular 21, you might have noticed something missing. You set up your schemas…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/angular-21-0-1-the-missing-style-link-in-signal-forms-bb8571e90f61?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fielddirective</category>
      <category>angular21</category>
      <category>dependencyinjection</category>
      <category>signalforms</category>
    </item>
    <item>
      <title>Angular 21: What’s new</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Fri, 21 Nov 2025 04:52:46 +0000</pubDate>
      <link>https://forem.com/amosisaila/angular-21-whats-new-1d8c</link>
      <guid>https://forem.com/amosisaila/angular-21-whats-new-1d8c</guid>
      <description>&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/angular-21-whats-new-29aebac7668a?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbfmjrs5vw4g7s6dyix68.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most significant shift is the zoneless-by-default approach. The future is reactive, performant, and decidedly signal-driven — and…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/angular-21-whats-new-29aebac7668a?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>signalforms</category>
      <category>ariapackage</category>
      <category>mcpai</category>
      <category>angular21</category>
    </item>
    <item>
      <title>Mastering the New debounce() API in Angular 21 Signal Forms</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Wed, 19 Nov 2025 05:33:02 +0000</pubDate>
      <link>https://forem.com/amosisaila/mastering-the-new-debounce-api-in-angular-21-signal-forms-4p0f</link>
      <guid>https://forem.com/amosisaila/mastering-the-new-debounce-api-in-angular-21-signal-forms-4p0f</guid>
      <description>&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/angulamastering-the-new-debounce-api-in-angular-21-signal-forms-00a72a49d1a2?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jk55crjf4nerb9jzxhl.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this comprehensive guide, we’ll explore the debounce() API through a real-world weather chatbot application, demonstrating how this…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/angulamastering-the-new-debounce-api-in-angular-21-signal-forms-00a72a49d1a2?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>angular21</category>
      <category>angularsignals</category>
      <category>angularsignalforms</category>
      <category>debounce</category>
    </item>
    <item>
      <title>Building Your Own MCP Server: Angular Best Practices Refactoring Tool — Part 2</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Tue, 04 Nov 2025 19:39:09 +0000</pubDate>
      <link>https://forem.com/amosisaila/building-your-own-mcp-server-angular-best-practices-refactoring-tool-part-2-19eb</link>
      <guid>https://forem.com/amosisaila/building-your-own-mcp-server-angular-best-practices-refactoring-tool-part-2-19eb</guid>
      <description>&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/building-your-own-mcp-server-angular-best-practices-refactoring-tool-part-2-ccf852d0de73?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqlpyiumnjz1kvgqs8421.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Part 1, we explored the theory behind Model Context Protocol, now it’s time to get our hands dirty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@amosisaila/building-your-own-mcp-server-angular-best-practices-refactoring-tool-part-2-ccf852d0de73?source=rss-e36e650e58ac------2" rel="noopener noreferrer"&gt;Continue reading on Medium »&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>angular</category>
      <category>mcpserver</category>
      <category>mcps</category>
    </item>
    <item>
      <title>Understanding Model Context Protocol (MCP) — Part 1</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Fri, 10 Oct 2025 18:57:23 +0000</pubDate>
      <link>https://forem.com/amosisaila/understanding-model-context-protocol-mcp-part-1-3bp6</link>
      <guid>https://forem.com/amosisaila/understanding-model-context-protocol-mcp-part-1-3bp6</guid>
      <description>&lt;h3&gt;
  
  
  Understanding Model Context Protocol (MCP) — Part 1
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faihh69xx5lmt2ai15qxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faihh69xx5lmt2ai15qxe.png" alt="Understanding Model Context Protocol (MCP) — Part 1" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Birth of a New Standard: How MCP Came to Be
&lt;/h3&gt;

&lt;p&gt;Large Language Models (LLMs) like ChatGPT, Claude, and Gemini can write code, debug complex problems, and explain technical concepts with remarkable fluency. Yet despite their brilliance, they’ve been operating in isolation-powerful minds trapped in a room with no doors or windows.&lt;/p&gt;

&lt;p&gt;Picture this: you have an AI assistant that can write flawless TypeScript, suggest perfect Angular component architectures, and identify bugs in milliseconds. But it can’t see your actual project files, can’t access your database, can’t read your API documentation, and has no idea what tools you’re using. It’s like hiring a world-class architect who has to design your house without ever seeing the plot of land, checking local building codes, or knowing what materials are available.&lt;/p&gt;

&lt;p&gt;This has been the fundamental limitation of AI development tools-until now.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Genesis: Anthropic’s Vision
&lt;/h3&gt;

&lt;p&gt;In November 2024, Anthropic — the AI safety company founded by former OpenAI researchers and creators of Claude — unveiled something that would change the landscape of AI integration: the &lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer"&gt;&lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiiy5o1te98i2dv8hwqzn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiiy5o1te98i2dv8hwqzn.png" alt="Model Context Protocol (MCP)" width="800" height="116"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The team at Anthropic had been grappling with a persistent problem. As they worked on making Claude more useful for real-world tasks, they kept running into the same wall: every time they wanted to connect Claude to a new tool, service, or data source, they had to build a custom integration from scratch. Database access required one approach. File system operations needed another. API integrations each demanded unique implementations.&lt;/p&gt;

&lt;p&gt;It was the API chaos problem we explored earlier, but at a massive scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Open Standard Revolution
&lt;/h3&gt;

&lt;p&gt;Rather than building yet another proprietary solution, Anthropic made a bold decision: they would create an open, universal protocol that any AI system could use to connect with any tool or service. On November 25, 2024, they released MCP as an open-source specification, complete with SDKs in Python and TypeScript.&lt;/p&gt;

&lt;p&gt;The core idea was elegant: instead of building point-to-point integrations between every AI model and every service, create a standardized “language” that both sides could speak. Think of it as establishing USB-C for AI connectivity-one protocol to rule them all.&lt;/p&gt;

&lt;p&gt;What made MCP truly revolutionary wasn’t just the technical specification. It was the philosophy behind it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open and Free&lt;/strong&gt; : Anyone could implement it, no licensing fees, no vendor lock-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional&lt;/strong&gt; : Both AI systems and services could initiate communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure by Design&lt;/strong&gt; : Built-in authorization and sandboxing capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple Yet Powerful&lt;/strong&gt; : Easy enough for individual developers to implement, robust enough for enterprise systems&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Unprecedented Adoption
&lt;/h3&gt;

&lt;p&gt;Here’s where the story gets really interesting. In the tech world, standards battles typically drag on for years. Companies fight over competing specifications, each pushing their own solution. But something different happened with MCP.&lt;/p&gt;

&lt;p&gt;By &lt;strong&gt;March 2025&lt;/strong&gt; , just four months after its introduction, OpenAI — Anthropic’s biggest competitor — announced they were adopting MCP for GPT integrations. Let that sink in: a company competing directly with Anthropic chose to embrace their open standard rather than build their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;April 2025&lt;/strong&gt; brought another bombshell: Google DeepMind integrated MCP support into their AI offerings. Microsoft followed in &lt;strong&gt;May 2025&lt;/strong&gt; , incorporating MCP into Azure AI services and announcing plans to support it across their development tools ecosystem.&lt;/p&gt;

&lt;p&gt;When fierce competitors this quickly agree on a standard, you’re witnessing something rare in tech history. It’s reminiscent of how HTTP became the universal protocol for the web, or how REST APIs became the de facto standard for web services.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Concept to Ecosystem
&lt;/h3&gt;

&lt;p&gt;Today, less than a year after its introduction, MCP has evolved from a novel idea into a thriving ecosystem. Development tools like Cursor, Windsurf, and Zed have built-in MCP support. Major SaaS companies are releasing official MCP servers. The community has created hundreds of integrations.&lt;/p&gt;

&lt;p&gt;What started as Anthropic’s solution to their own integration challenges has become the foundation for how AI systems interact with the digital world.&lt;/p&gt;

&lt;p&gt;And that’s exactly what we’re going to explore in this article: understanding the principles behind MCP and learning how to build our own MCP server using Angular, joining this revolution in AI integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fundamental Problem: AI That Can Only “Talk”
&lt;/h3&gt;

&lt;p&gt;When you ask ChatGPT to “plan me a week-long holiday in Costa Rica,” it can tell you exactly what to do, suggest amazing destinations, even help you create detailed itineraries — but it cannot actually open a browser, navigate to travel sites, check real-time availability, or complete bookings. LLMs are fundamentally text generators, not action-takers. This is essentially the limitation that Large Language Models (LLMs) like GPT face today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7bigd4btrukt1hovzb6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7bigd4btrukt1hovzb6.png" alt="LLM has limitations" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter AI Agents: The Hands and Feet of AI
&lt;/h3&gt;

&lt;p&gt;This is where AI agents come into play. Think of an AI agent as an intelligent assistant that combines the reasoning power of an LLM with the ability actually to &lt;em&gt;do&lt;/em&gt; things. It’s like giving that brilliant person in the room a set of tools, internet access, and the ability to interact with the outside world.&lt;/p&gt;

&lt;p&gt;An AI agent follows a simple but powerful pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Receive a request&lt;/strong&gt; from the user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think about it&lt;/strong&gt; using an LLM (“What kind of holiday experience does the user want?”)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take actions&lt;/strong&gt; by calling external services and APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think again&lt;/strong&gt; about the results (“Do these options match their preferences? Do I need more information?”)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat&lt;/strong&gt; until the task is complete&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtlnuujk9wtkondrl2y6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtlnuujk9wtkondrl2y6.png" alt="AI Cycle with Agent" width="601" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, when you say “plan me an adventure holiday in Costa Rica,” an AI agent might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask the LLM to extract details (budget, interests, group size, dates)&lt;/li&gt;
&lt;li&gt;Call tourism APIs to find available activities and accommodations&lt;/li&gt;
&lt;li&gt;Check weather services for seasonal considerations&lt;/li&gt;
&lt;li&gt;Ask the LLM to analyze options against your adventure preferences&lt;/li&gt;
&lt;li&gt;Present a complete itinerary with bookable options&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The API Chaos Problem
&lt;/h3&gt;

&lt;p&gt;Here’s where things get complicated. Every travel service has its own API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Airbnb uses /v2/listings/search&lt;/li&gt;
&lt;li&gt;TripAdvisor uses /experiences/search&lt;/li&gt;
&lt;li&gt;National Parks Service uses /parks/activities&lt;/li&gt;
&lt;li&gt;Local tour operators each have unique endpoints
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The nightmare of integrating multiple travel APIs - each one is different!

class HolidayPlanningAgent {
  async findAccommodations(destination, checkin, checkout, guests) {
    let results = [];

    // Airbnb - uses Bearer token, POST request
    const airbnbResponse = await fetch(`https://api.airbnb.com/v2/listings/search`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${this.airbnbToken}` },
      body: JSON.stringify({
        location: destination, // calls it 'location'
        check_in: checkin, // underscore format
        adults: guests
      })
    });
    // Returns: { listings: [{ price_per_night, coordinates: {lat, lng} }] }

    // Booking.com - uses Basic auth, GET request  
    const bookingResponse = await fetch(`https://distribution-xml.booking.com/2.4/json/hotelAvailability`, {
      method: 'GET',
      headers: { 'Authorization': `Basic ${this.bookingAuth}` },
      params: {
        city: destination, // calls it 'city'
        checkin_date: checkin, // different naming
        room1: `A,A,${guests}` // completely different format
      }
    });
    // Returns: { result: [{ min_total_price, latitude, longitude }] }

    // Hotels.com - uses API key, different date format
    const hotelsResponse = await fetch(`https://api.ean.com/ean-services/rs/hotel/v3/list`, {
      headers: { 'Authorization': `EAN APIKey=${this.hotelsKey}` },
      params: {
        destinationString: destination, // yet another name
        arrivalDate: checkin.replace(/-/g, '/'), // wants MM/DD/YYYY format
        RoomGroup: `(A,${guests})` // parentheses format
      }
    });
    // Returns: { HotelListResponse: { HotelList: { HotelSummary: [{ lowRate }] } } }

    return results; // Each API returns completely different data structures
  }

  async findActivities(destination, type) {
    // TripAdvisor
    await fetch(`https://api.tripadvisor.com/api/partner/2.0/experiences/search`, {
      params: { location: destination, category: this.mapToTripAdvisorCategory(type) }
    });
    // Returns: { data: [{ experience_id, title, price_from }] }
    // GetYourGuide  
    await fetch(`https://www.getyourguide.com/api/v1/activities`, {
      method: 'POST',
      body: JSON.stringify({
        data: { type: 'activity-search', attributes: { destination } }
      })
    });
    // Returns JSON API format: { data: [{ id, attributes: { title, price: { amount } } }] }
    // National Parks
    await fetch(`https://developer.nps.gov/api/v1/activities/parks`, {
      params: { q: destination } // calls it 'q' instead of 'destination'
    });
    // Returns: { data: [{ parks: [{ fullName, entranceFees: [{ cost }] }] }] }
  }

  // Every API needs different category mappings
  mapToTripAdvisorCategory(type) {
    return { 'adventure': 'outdoor-activities', 'culture': 'cultural-tours' }[type];
  }
}

/*
The Problem: 
- 6 different authentication methods (Bearer, Basic, API Key)
- 6 different request formats (POST vs GET, different JSON structures)
- 6 different parameter names for the same concept (location vs city vs destinationString vs q)
- 6 different response formats to parse and normalize
- 6 different error handling approaches
- Constant maintenance as each API evolves independently
Result: You spend more time on API integration than actual holiday planning logic!
*/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each returns data in different formats, requires different authentication, and has different parameter names. If you want your AI agent to work with hundreds of accommodation providers, activity companies, restaurants, and attractions, you’d need to write custom integration code for each one.&lt;/p&gt;

&lt;p&gt;This is like having a universal remote control that needs a different button layout for every device — it’s technically possible but practically unsustainable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model Context Protocol: The Universal Translator
&lt;/h3&gt;

&lt;p&gt;Model Context Protocol (MCP) solves this problem by creating a standardized way for AI agents to communicate with external services. Instead of writing custom code for each travel API, you create an MCP server that acts as a translator between the standardized MCP language and the service’s specific API.&lt;/p&gt;

&lt;p&gt;Think of MCP as creating a universal interface. Instead of your AI agent needing to learn 1,000 different ways to say “find adventure activities,” there’s now one standard way that works with any MCP-compatible service.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Three Pillars of MCP
&lt;/h3&gt;

&lt;p&gt;Every MCP server exposes three types of capabilities:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Tools — The Actions
&lt;/h3&gt;

&lt;p&gt;Tools are the things your AI agent can &lt;em&gt;do&lt;/em&gt;. These are functions that perform actions or retrieve data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search_accommodations(location, checkin, checkout, guests)&lt;/li&gt;
&lt;li&gt;find_activities(destination, activity_type, difficulty_level)&lt;/li&gt;
&lt;li&gt;get_restaurant_recommendations(location, cuisine, budget)&lt;/li&gt;
&lt;li&gt;check_weather_forecast(destination, dates)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Resources — The Context
&lt;/h3&gt;

&lt;p&gt;Resources are static information that helps the AI make better decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Destination guides and local customs&lt;/li&gt;
&lt;li&gt;Activity difficulty ratings and requirements&lt;/li&gt;
&lt;li&gt;Seasonal travel recommendations&lt;/li&gt;
&lt;li&gt;Visa and vaccination requirements&lt;/li&gt;
&lt;li&gt;Currency exchange rates and tipping guides&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Prompts — The Wisdom
&lt;/h3&gt;

&lt;p&gt;Prompts are pre-written instructions that help the AI use the tools correctly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“When planning adventure trips, always check weather patterns and seasonal accessibility”&lt;/li&gt;
&lt;li&gt;“For family holidays, prioritize activities suitable for all age groups mentioned”&lt;/li&gt;
&lt;li&gt;“When suggesting accommodations, consider proximity to planned activities”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Architecture: Client Meets Server
&lt;/h3&gt;

&lt;p&gt;MCP follows a simple client-server model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP Client&lt;/strong&gt; (your AI agent or IDE):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discovers what an MCP server can do&lt;/li&gt;
&lt;li&gt;Sends requests to perform actions&lt;/li&gt;
&lt;li&gt;Receives responses and data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MCP Server&lt;/strong&gt; (the service provider):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exposes tools, resources, and prompts&lt;/li&gt;
&lt;li&gt;Handles the actual API calls to external services&lt;/li&gt;
&lt;li&gt;Returns standardized responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The communication happens through JSON-RPC, a lightweight protocol that’s like having a standardized conversation format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client: "Can you search for adventure activities?"
Server: "Yes, I can do that. Give me destination, activity type, and difficulty level."
Client: "Find zip-lining activities in Costa Rica for intermediate level"
Server: "Here are 8 zip-lining tours matching your criteria..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Communication Protocol: JSON-RPC 2.0
&lt;/h3&gt;

&lt;p&gt;Before diving into the MCP specification itself, we need to understand how MCP clients and servers actually communicate with each other. MCP uses JSON-RPC 2.0 as its communication protocol — think of it as the “language” that clients and servers speak when exchanging information.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is JSON-RPC?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.jsonrpc.org/specification" rel="noopener noreferrer"&gt;JSON-RPC&lt;/a&gt; ( &lt;strong&gt;JSON Remote Procedure Call&lt;/strong&gt; ) is a lightweight, stateless protocol that allows a client to call methods on a remote server and receive responses. It’s essentially a standardized way to say “run this function with these parameters” across a network or between processes.&lt;/p&gt;

&lt;p&gt;The “2.0” refers to the specification version, which defines exactly how requests and responses should be formatted. This standardization ensures that any JSON-RPC 2.0 client can communicate with any JSON-RPC 2.0 server, regardless of what programming language they’re built with.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fge8bpa9pliwipwnx4ywt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fge8bpa9pliwipwnx4ywt.png" alt="JSON-RPC protocol v2" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Basic Structure
&lt;/h3&gt;

&lt;p&gt;Every &lt;a href="https://en.wikipedia.org/wiki/JSON-RPC#Implementations" rel="noopener noreferrer"&gt;JSON-RPC request&lt;/a&gt; must include four elements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// JSON-RPC 2.0 Request Structure
{
  "jsonrpc": "2.0",
  "method": "search_hotels",
  "params": {
    "destination": "Costa Rica",
    "checkin": "2024-12-15",
    "checkout": "2024-12-22",
    "guests": 2
  },
  "id": 1
}

// JSON-RPC 2.0 Response Structure (Success)
{
  "jsonrpc": "2.0",
  "result": {
    "hotels": [
      {
        "id": "hotel_123",
        "name": "Rainforest Lodge",
        "price_per_night": 150,
        "location": "Manuel Antonio",
        "rating": 4.5
      },
      {
        "id": "hotel_456", 
        "name": "Beach Resort",
        "price_per_night": 200,
        "location": "Guanacaste",
        "rating": 4.2
      }
    ]
  },
  "id": 1
}

// JSON-RPC 2.0 Response Structure (Error)
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": "Missing required parameter: destination"
  },
  "id": 1
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Transport Mechanisms
&lt;/h3&gt;

&lt;p&gt;JSON-RPC 2.0 is transport-agnostic, meaning the protocol doesn’t dictate how messages are delivered. MCP supports two primary transport methods:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Standard I/O (stdio)&lt;/strong&gt;: Perfect for local development and testing. The MCP server runs as a subprocess, and messages are exchanged through standard input/output pipes. This is lightweight, secure, and ideal for IDEs like Cursor or VS Code extensions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP&lt;/strong&gt; : Used when the MCP server is hosted remotely or needs to serve multiple clients simultaneously. Messages are sent as HTTP POST requests, making it easy to deploy MCP servers as web services.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP-Specific Conventions
&lt;/h3&gt;

&lt;p&gt;While MCP uses standard JSON-RPC 2.0, it defines specific method names and parameter structures:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery Methods&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tools/list - Get available tools&lt;/li&gt;
&lt;li&gt;resources/list - Get available resources&lt;/li&gt;
&lt;li&gt;prompts/list - Get available prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Action Methods&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tools/call - Execute a specific tool&lt;/li&gt;
&lt;li&gt;resources/read - Retrieve resource content&lt;/li&gt;
&lt;li&gt;prompts/get - Get a specific prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Notification Methods&lt;/strong&gt; (no response expected):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;notifications/initialized - Client signals it's ready&lt;/li&gt;
&lt;li&gt;notifications/progress - Server reports progress updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;

&lt;p&gt;JSON-RPC 2.0 defines standard error codes that MCP extends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;-32700 Parse error (invalid JSON)&lt;/li&gt;
&lt;li&gt;-32600 Invalid request (malformed JSON-RPC)&lt;/li&gt;
&lt;li&gt;-32601 Method not found&lt;/li&gt;
&lt;li&gt;-32602 Invalid params&lt;/li&gt;
&lt;li&gt;-32603 Internal error&lt;/li&gt;
&lt;li&gt;-32000 to -32099 Server-defined errors (MCP uses these for domain-specific issues)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This standardized approach means client applications can handle errors consistently across different MCP servers.&lt;/p&gt;

&lt;p&gt;The beauty of this protocol choice is that it abstracts away all the complexity we saw in our API chaos examples. Instead of learning different authentication schemes, parameter formats, and response structures for each service, developers work with one consistent JSON-RPC interface while MCP servers handle the underlying API complexity behind the scenes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ecosystem Effect
&lt;/h3&gt;

&lt;p&gt;The real power of MCP emerges when it becomes widespread. Imagine a world where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every major travel platform provides an MCP server&lt;/li&gt;
&lt;li&gt;Tourism boards offer MCP servers for their destinations&lt;/li&gt;
&lt;li&gt;AI development tools automatically discover and connect to travel MCP servers&lt;/li&gt;
&lt;li&gt;Building a holiday planning agent is as simple as configuring which destinations and services you want to include&lt;/li&gt;
&lt;li&gt;Your agent can seamlessly work with any new tourism service that supports MCP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’re moving toward a future where AI agents can plan and coordinate complex holiday experiences as easily as humans browse travel websites — and MCP is the standard that makes it possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s Next?
&lt;/h3&gt;

&lt;p&gt;Now that you understand the foundational concepts of MCP, you’re ready to dive into the technical implementation. In the next article, we’ll explore how to build your own MCP server, create custom tools, and integrate everything into a working AI agent system (basically into our Angular project).&lt;/p&gt;

&lt;p&gt;The beauty of MCP is that once you understand these core concepts, the implementation becomes a matter of following the specification — and that’s where the real fun begins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thanks for reading so far 🙏
&lt;/h3&gt;

&lt;p&gt;I’d like to have your feedback so please leave a &lt;strong&gt;comment&lt;/strong&gt; , &lt;strong&gt;clap&lt;/strong&gt; or &lt;strong&gt;follow&lt;/strong&gt;. &lt;em&gt;👏&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spread the Angular love! 💜&lt;/p&gt;

&lt;p&gt;If you really liked it, &lt;strong&gt;share it&lt;/strong&gt; among your community, tech bros and whoever you want! 🚀👥&lt;/p&gt;

&lt;p&gt;Don’t forget to follow me and stay updated: 📱&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://www.linkedin.com/in/amos-lucian-isaila-34ab78146/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;a href="https://codigotipado.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Newsletter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@codigotipado" rel="noopener noreferrer"&gt;&lt;strong&gt;YouTube&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;a href="https://twitter.com/amosisaila" rel="noopener noreferrer"&gt;&lt;strong&gt;Twitter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for being part of this Angular journey! 👋😁&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a href="https://www.codigotipado.com/p/understanding-model-context-protocol" rel="noopener noreferrer"&gt;&lt;em&gt;https://www.codigotipado.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>modelcontextprotocol</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>Mastering Angular 21 Signal Forms: A Deep Dive into the Experimental API</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Thu, 02 Oct 2025 05:33:53 +0000</pubDate>
      <link>https://forem.com/amosisaila/mastering-angular-21-signal-forms-a-deep-dive-into-the-experimental-api-b23</link>
      <guid>https://forem.com/amosisaila/mastering-angular-21-signal-forms-a-deep-dive-into-the-experimental-api-b23</guid>
      <description>&lt;p&gt;Angular 21 introduces one of the most significant improvements to form handling since the framework’s inception: Signal-Based Forms (Experimental).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4iw5er43d18tqkweyh3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc4iw5er43d18tqkweyh3.png" alt="Angular 21 Signal Forms API" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;Traditional Angular forms, while powerful, have long suffered from several pain points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verbose boilerplate&lt;/strong&gt; with FormBuilder, FormGroup, and FormControl&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex state management&lt;/strong&gt; requiring manual subscriptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance overhead&lt;/strong&gt; from Observable chains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cumbersome validation&lt;/strong&gt; error handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Difficult form synchronization&lt;/strong&gt; with component state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this comprehensive guide, we’ll transform a real-world weather chatbot application from reactive forms to Signal Forms, showcasing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete migration strategies&lt;/li&gt;
&lt;li&gt;Performance improvements&lt;/li&gt;
&lt;li&gt;Advanced validation techniques&lt;/li&gt;
&lt;li&gt;Best practices for Signal Forms&lt;/li&gt;
&lt;li&gt;When and how to adopt this new approach&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdtzes08ckj4z7f58r7o9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdtzes08ckj4z7f58r7o9.png" alt="Weather App Using Signal Forms in Angular" width="800" height="941"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the &lt;a href="https://github.com/amosISA/angular-signal-forms" rel="noopener noreferrer"&gt;&lt;strong&gt;source code of the Weather ChatBot App here&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reactive Forms Pain Points
&lt;/h3&gt;

&lt;p&gt;Let’s examine our weather chatbot application to understand the current challenges with reactive forms.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Weather Chatbot Example
&lt;/h3&gt;

&lt;p&gt;Our application features a weather query form with the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Date&lt;/strong&gt; : When to check the weather&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Country&lt;/strong&gt; : Location country&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;City&lt;/strong&gt; : Specific city&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temperature Unit&lt;/strong&gt; : Celsius or Fahrenheit preference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the current reactive forms implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// weather-chatbot.component.ts
@Component({
  selector: ‘app-weather-chatbot’,
  templateUrl: ‘./weather-chatbot.component.html’,
  imports: [CommonModule, ReactiveFormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WeatherChatbotComponent {
  private readonly _formBuilder = inject(FormBuilder);
  private readonly _chatService = inject(ChatService);

  protected readonly messages = signal&amp;lt;ChatMessage[]&amp;gt;([]);
  protected readonly isSubmitting = signal(false);

  protected readonly messageCount = computed(() =&amp;gt; this.messages().length);
  protected readonly formValue = computed(() =&amp;gt; this.weatherForm.value);
  protected readonly isDevelopment = signal(false);

  // Traditional Reactive Form
  protected readonly weatherForm: FormGroup = this._formBuilder.group({
    date: [’‘, Validators.required],
    country: [’‘, [Validators.required, Validators.minLength(2)]],
    city: [’‘, [Validators.required, Validators.minLength(2)]],
    temperatureUnit: [’celsius’, Validators.required] as [TemperatureUnit, any],
  });

  constructor() {
    // Manual form initialization
    const today = new Date().toISOString().split(’T’)[0];
    this.weatherForm.patchValue({ date: today });
  }

  protected onSubmitWeatherQuery(): void {
    // Manual form validation handling
    if (this.weatherForm.invalid) {
      this.weatherForm.markAllAsTouched();
      return;
    }

    const formData = this.weatherForm.value as WeatherFormData;
    const query = this._buildWeatherQuery(formData);

    this._addUserMessage(query);
    this._sendMessageToAI(query);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Template Pain Points
&lt;/h3&gt;

&lt;p&gt;The template reveals additional complexity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- Complex validation error handling --&amp;gt;
@if (weatherForm.get(’country’)?.errors &amp;amp;&amp;amp; weatherForm.get(’country’)?.touched) {
  &amp;lt;p class=”text-red-500 text-xs mt-1”&amp;gt;
    @if (weatherForm.get(’country’)?.errors?.[’required’]) { 
      Country is required 
    } 
    @if (weatherForm.get(’country’)?.errors?.[’minlength’]) { 
      Country must be at least 2 characters 
    }
  &amp;lt;/p&amp;gt;
}

&amp;lt;!-- Verbose form control access --&amp;gt;
&amp;lt;input
  id=”country”
  type=”text”
  formControlName=”country”
  placeholder=”e.g., United States”
  class=”w-full px-3 py-2 border border-gray-300 rounded-lg...”
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Identified Pain Points
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Boilerplate Overload&lt;/strong&gt; : FormBuilder, FormGroup, manual validation setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type Safety Issues&lt;/strong&gt; : weatherForm.value as WeatherFormData casting required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex Error Handling&lt;/strong&gt; : Nested conditionals for validation messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Synchronization&lt;/strong&gt; : Manual form value computed signals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verbose Template Logic&lt;/strong&gt; : Repeated weatherForm.get() calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed Paradigms&lt;/strong&gt; : Signals for app state, Observables for forms&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Signal Forms API Explained
&lt;/h3&gt;

&lt;p&gt;Before diving into the migration, understanding the core Signal Forms API is essential. This section covers the key functions, types, and patterns you’ll use when building forms with Angular 21’s experimental Signal Forms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mermaidchart.com/d/986ec4e0-c63e-4327-9bc6-c7c8e53fdc9e" rel="noopener noreferrer"&gt;&lt;strong&gt;Here is a diagram showing a high-level overview of Angular signal forms.&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Form Creation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  form(model, schema?, options?)
&lt;/h4&gt;

&lt;p&gt;Creates a Signal Form bound to a data model. Updating the FieldState (form fields) updates also the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parameters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;model: WritableSignal - The data signal that serves as the source of truth&lt;/li&gt;
&lt;li&gt;schema?: SchemaOrSchemaFn - Optional validation and logic rules&lt;/li&gt;
&lt;li&gt;options?: FormOptions - Optional configuration (injector, name, adapter)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Returns:&lt;/strong&gt; Field - A reactive field tree matching your data structure&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Basic Form (No Schema)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const data = signal({
  username: ‘’,
  email: ‘’
});

const basicForm = form(data);

// Access fields
basicForm.username().value(); // Read value
basicForm.username().value.set(’john’); // Write value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Form with Inline Schema Function (Most common pattern for defining validation inline)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const userForm = form(
  signal({ username: ‘’, email: ‘’, age: 0 }),
  (path) =&amp;gt; {
    // path represents the root of your data structure
    required(path.username);
    required(path.email);
    email(path.email);
    min(path.age, 18, { message: ‘Must be 18 or older’ });
  }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The&lt;/strong&gt; path &lt;strong&gt;parameter:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type: FieldPath&lt;/li&gt;
&lt;li&gt;Represents the root field&lt;/li&gt;
&lt;li&gt;Provides type-safe navigation to all nested properties&lt;/li&gt;
&lt;li&gt;Used to target which fields get which rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The schema function parameter is called path because it represents a &lt;strong&gt;location&lt;/strong&gt; or &lt;strong&gt;path&lt;/strong&gt; in your data structure. Think of it like navigating a file system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Just like file paths:
// /user/profile/name
// /user/profile/email

// Signal Forms paths:
// path.user.profile.name
// path.user.profile.email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path object is a &lt;strong&gt;proxy&lt;/strong&gt; that mirrors your data model’s structure. When you write path.username, you’re not accessing actual data—you’re defining &lt;strong&gt;where&lt;/strong&gt; in the form tree to apply validation rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This is NOT data access:
const myForm = form(data, (path) =&amp;gt; {
  required(path.username); // path.username is a “path marker”, not a value
});

// This IS data access:
const actualValue = myForm.username().value(); // Reading the actual data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;path is directly related to PathKind. It’s a &lt;strong&gt;type-level marker&lt;/strong&gt; that Angular uses to ensure you’re using validators and logic functions correctly based on &lt;strong&gt;where&lt;/strong&gt; in the form tree they’re applied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is PathKind?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PathKind classifies paths into three categories based on their position in the form structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type PathKind = 
  | PathKind.Root // Top-level form path
  | PathKind.Child // Nested property path  
  | PathKind.Item // Array element path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PathKind.Root&lt;/strong&gt;  — The Entry Point&lt;/p&gt;

&lt;p&gt;This is the path parameter you receive in your main schema function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const myForm = form(signal(data), (path) =&amp;gt; {
  // ↑ path is PathKind.Root
  // This is the root of your form tree

  required(path.username); // username is Child
  required(path.email); // email is Child
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The starting point of navigation&lt;/li&gt;
&lt;li&gt;Accepts functions designed for Root, Child, or Item paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PathKind.Child&lt;/strong&gt;  — Nested Properties&lt;/p&gt;

&lt;p&gt;Any property accessed from another path becomes a Child:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type User = {
  profile: {
    firstName: string;
    lastName: string;
  };
};

const userForm = form(signal&amp;lt;User&amp;gt;(...), (path) =&amp;gt; {
  // path = Root
  // path.profile = Child
  // path.profile.firstName = Child
  // path.profile.lastName = Child

  required(path.profile.firstName); // Child path
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accessed via dot notation from another path&lt;/li&gt;
&lt;li&gt;Represents a specific property in the data structure&lt;/li&gt;
&lt;li&gt;Can be further navigated if the value is an object&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PathKind.Item&lt;/strong&gt;  — Array Elements&lt;/p&gt;

&lt;p&gt;Array items get special treatment with PathKind.Item:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type TodoList = {
  todos: Array&amp;lt;{
    title: string;
    done: boolean;
  }&amp;gt;;
};

const todoForm = form(signal&amp;lt;TodoList&amp;gt;(...), (path) =&amp;gt; {
  // path = Root
  // path.todos = Child (the array itself)

  applyEach(path.todos, (itemPath) =&amp;gt; {
    // itemPath = Item (one element in the array)
    // itemPath.title = Child (of the Item)
    // itemPath.done = Child (of the Item)

    required(itemPath.title);
    // itemPath has access to special item context
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only created through applyEach&lt;/li&gt;
&lt;li&gt;Represents a single element in an array&lt;/li&gt;
&lt;li&gt;Has access to additional context (like index)&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Form with Predefined Schema (Reusable schemas for consistent validation across forms):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Define schema once
const userSchema = schema&amp;lt;User&amp;gt;((path) =&amp;gt; {
  required(path.username);
  minLength(path.username, 3);
  email(path.email);
});

// Reuse across multiple forms
const registrationForm = form(signal(newUser), userSchema);
const profileForm = form(signal(currentUser), userSchema);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Form with Options
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const myForm = form(
  signal(data),
  (path) =&amp;gt; {
    required(path.name);
  },
  {
    injector: customInjector, // Custom DI context
    name: ‘user-registration’, // Form identifier for debugging
    adapter: customFieldAdapter // Advanced customization
  }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Understanding the&lt;/strong&gt; adapter ** Option**&lt;/p&gt;

&lt;p&gt;The adapter option allows you to &lt;strong&gt;customize how fields are created and managed internally&lt;/strong&gt;. This is an advanced, low-level API that most developers will never need to touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does an adapter do?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It controls the internal lifecycle of form fields by implementing the FieldAdapter interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface FieldAdapter {
  // How to create the field structure (parent-child relationships)
  createStructure(node: FieldNode, options: FieldNodeOptions): FieldNodeStructure;

  // How to create validation state (errors, valid, pending, etc.)
  createValidationState(node: FieldNode, options: FieldNodeOptions): ValidationState;

  // How to create field state (touched, dirty, disabled, etc.)
  createNodeState(node: FieldNode, options: FieldNodeOptions): FieldNodeState;

  // How to create child field nodes
  newChild(options: ChildFieldNodeOptions): FieldNode;

  // How to create root field nodes
  newRoot&amp;lt;TValue&amp;gt;(
    fieldManager: FormFieldManager,
    model: WritableSignal&amp;lt;TValue&amp;gt;,
    pathNode: FieldPathNode,
    adapter: FieldAdapter
  ): FieldNode;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Would You Use a Custom Adapter?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reactive Forms Compatibility Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the primary use case mentioned in the source code. If you’re migrating from reactive forms and need both systems to coexist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Hypothetical compatibility adapter
class ReactiveFormsAdapter implements FieldAdapter {
  createValidationState(node: FieldNode, options: FieldNodeOptions): ValidationState {
    // Return a validation state that also updates reactive forms validators
    return new HybridValidationState(node, this.reactiveFormControl);
  }

  // ... other methods
}

const hybridForm = form(
  signal(data),
  schema,
  { adapter: new ReactiveFormsAdapter(existingFormGroup) }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Testing and Debugging&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create adapters that expose additional debugging information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DebugAdapter implements FieldAdapter {
  private _fieldLog = new Map&amp;lt;string, any[]&amp;gt;();

  newRoot&amp;lt;TValue&amp;gt;(
    fieldManager: FormFieldManager,
    model: WritableSignal&amp;lt;TValue&amp;gt;,
    pathNode: FieldPathNode,
    adapter: FieldAdapter
  ): FieldNode {
    const node = FieldNode.newRoot(fieldManager, model, pathNode, adapter);

    // Track all field accesses
    this._fieldLog.set(’root’, []);
    effect(() =&amp;gt; {
      this.fieldLog.get(’root’)?.push({
        timestamp: Date.now(),
        value: model()
      });
    });

    return node;
  }

  // ... other methods
}

// Use in tests
const testForm = form(signal(data), schema, { 
  adapter: new DebugAdapter() 
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  schema(fn: SchemaFn)
&lt;/h4&gt;

&lt;p&gt;Creates a reusable schema (adds logic rules to a form) that can be applied to multiple forms or composed with other schemas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parameters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fn: SchemaFn - A function that defines validation and logic rules ( &lt;strong&gt;non-reactive&lt;/strong&gt; function)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Returns:&lt;/strong&gt; Schema - A reusable schema object&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const addressSchema = schema&amp;lt;Address&amp;gt;((path) =&amp;gt; {
  required(path.street);
  required(path.city);
  minLength(path.zipCode, 5);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Field Types
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Field
&lt;/h4&gt;

&lt;p&gt;Represents a single field in the form. Acts as both a function and an object with subfields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call as function to access state: myForm() returns FieldState&lt;/li&gt;
&lt;li&gt;Navigate as object: myForm.name accesses nested fields&lt;/li&gt;
&lt;li&gt;Arrays are iterable: for (let item of myForm.items)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  FieldState
&lt;/h4&gt;

&lt;p&gt;The reactive state of a field, accessed by calling the field as a function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core properties:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface FieldState&amp;lt;TValue&amp;gt; {
  value: WritableSignal&amp;lt;TValue&amp;gt;; // Read/write the field value
  errors: Signal&amp;lt;ValidationError[]&amp;gt;; // Current validation errors
  errorSummary: Signal&amp;lt;ValidationError[]&amp;gt;; // Errors including descendants
  valid: Signal&amp;lt;boolean&amp;gt;; // True if no errors and no pending validators
  invalid: Signal&amp;lt;boolean&amp;gt;; // True if has errors (regardless of pending)
  pending: Signal&amp;lt;boolean&amp;gt;; // True if async validators running
  touched: Signal&amp;lt;boolean&amp;gt;; // True if field has been blurred
  dirty: Signal&amp;lt;boolean&amp;gt;; // True if value has been changed
  disabled: Signal&amp;lt;boolean&amp;gt;; // True if field is disabled
  readonly: Signal&amp;lt;boolean&amp;gt;; // True if field is readonly
  hidden: Signal&amp;lt;boolean&amp;gt;; // True if field is hidden
  submitting: Signal&amp;lt;boolean&amp;gt;; // True if form is submitting
  name: Signal&amp;lt;string&amp;gt;; // Unique field name

  // Methods
  markAsTouched(): void;
  markAsDirty(): void;
  reset(): void;
  property&amp;lt;M&amp;gt;(prop: Property&amp;lt;M&amp;gt; | AggregateProperty&amp;lt;M, any&amp;gt;): M | Signal&amp;lt;M&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Control Binding
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Control Directive
&lt;/h4&gt;

&lt;p&gt;Binds a Field to a UI control element.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;input [control]=”myForm.fieldName” /&amp;gt;
&amp;lt;textarea [control]=”myForm.description” /&amp;gt;
&amp;lt;select [control]=”myForm.category” /&amp;gt;
&amp;lt;input type=”checkbox” [control]=”myForm.accepted” /&amp;gt;
&amp;lt;input type=”radio” [control]=”myForm.option” value=”a” /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically syncs field value with control&lt;/li&gt;
&lt;li&gt;Binds validation state (invalid, touched, etc.)&lt;/li&gt;
&lt;li&gt;Handles disabled/readonly/hidden states&lt;/li&gt;
&lt;li&gt;Works with native inputs and custom controls&lt;/li&gt;
&lt;li&gt;Provides fake NgControl for reactive forms compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Built-in Validators
&lt;/h3&gt;

&lt;p&gt;All validators follow this pattern: validator(path, value?, config?)&lt;/p&gt;

&lt;h4&gt;
  
  
  required(path, config?)
&lt;/h4&gt;

&lt;p&gt;Ensures field has a non-empty value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;required(path.name);
required(path.email, { message: ‘Email is required’ });
required(path.terms, { 
  when: (ctx) =&amp;gt; ctx.valueOf(path.needsConsent) 
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Also sets:&lt;/strong&gt; REQUIRED aggregate property&lt;/p&gt;

&lt;h4&gt;
  
  
  minLength(path, length, config?)
&lt;/h4&gt;

&lt;p&gt;Validates minimum string/array length.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;minLength(path.password, 8);
minLength(path.username, 3, { message: ‘Too short’ });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Also sets:&lt;/strong&gt; MIN_LENGTH aggregate property&lt;/p&gt;

&lt;h4&gt;
  
  
  maxLength(path, length, config?)
&lt;/h4&gt;

&lt;p&gt;Validates maximum string/array length.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;maxLength(path.bio, 500);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Also sets:&lt;/strong&gt; MAX_LENGTH aggregate property&lt;/p&gt;

&lt;h4&gt;
  
  
  min(path, value, config?)
&lt;/h4&gt;

&lt;p&gt;Validates minimum numeric value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;min(path.age, 18);
min(path.price, 0, { message: ‘Price cannot be negative’ });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Also sets:&lt;/strong&gt; MIN aggregate property&lt;/p&gt;

&lt;h4&gt;
  
  
  max(path, value, config?)
&lt;/h4&gt;

&lt;p&gt;Validates maximum numeric value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max(path.quantity, 100);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Also sets:&lt;/strong&gt; MAX aggregate property&lt;/p&gt;

&lt;h4&gt;
  
  
  pattern(path, regex, config?)
&lt;/h4&gt;

&lt;p&gt;Validates against a regular expression.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pattern(path.phone, /^\d{3}-\d{3}-\d{4}$/);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Also sets:&lt;/strong&gt; PATTERN aggregate property&lt;/p&gt;

&lt;h4&gt;
  
  
  email(path, config?)
&lt;/h4&gt;

&lt;p&gt;Validates email format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email(path.email);
email(path.email, { message: ‘Invalid email address’ });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom Validation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  validate(path, validator)
&lt;/h4&gt;

&lt;p&gt;Adds a custom synchronous validator for a single field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate(path.username, (ctx) =&amp;gt; {
  const value = ctx.value();
  if (value.includes(’ ‘)) {
    return customError({ 
      kind: ‘no_spaces’,
      message: ‘Username cannot contain spaces’ 
    });
  }
  return null; // No error
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Validator return types:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;null | undefined | void - No error&lt;/li&gt;
&lt;li&gt;ValidationError - Single error&lt;/li&gt;
&lt;li&gt;ValidationError[] - Multiple errors&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  validateTree(path, validator)
&lt;/h4&gt;

&lt;p&gt;Adds a validator that can target multiple fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validateTree(path, (ctx) =&amp;gt; {
  const from = ctx.field.from().value();
  const to = ctx.field.to().value();

  if (from === to) {
    return {
      kind: ‘same_location’,
      field: ctx.field.from, // Target specific field
      message: ‘Departure and arrival cannot be the same’
    };
  }
  return null;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  validateAsync(path, options)
&lt;/h4&gt;

&lt;p&gt;For validation that requires server calls or time-consuming operations, use async validators.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { rxResource } from ‘@angular/core/rxjs-interop’;
import { of, delay, map } from ‘rxjs’;

validateAsync(path.username, {
  // Map field state to resource parameters
  params: (ctx) =&amp;gt; ({
    username: ctx.value()
  }),

  // Create resource with those parameters
  factory: (params) =&amp;gt; {
    return rxResource({
      request: () =&amp;gt; params().username,
      loader: ({ request: username }) =&amp;gt; {
        // Simulate API call
        return of(null).pipe(
          delay(1000),
          map(() =&amp;gt; checkUsernameAvailability(username))
        );
      }
    });
  },

  // Map resource result to errors
  errors: (result, ctx) =&amp;gt; {
    if (!result.available) {
      return customError({
        kind: 'username_taken',
        message: `Username "${ctx.value()}" is already taken`,
        suggestions: result.suggestions
      });
    }
    return null;
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  validateHttp(path, options)
&lt;/h4&gt;

&lt;p&gt;Simplified async validation for HTTP requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validateHttp(path.email, {
  // Return URL or HttpResourceRequest
  request: (ctx) =&amp;gt; ({
    url: ‘/api/validate-email’,
    params: { email: ctx.value() }
  }),

  // Map response to errors
  errors: (result, ctx) =&amp;gt; {
    if (!result.valid) {
      return customError({
        kind: ‘invalid_email_server’,
        message: result.message || ‘Email validation failed’,
        details: result.details
      });
    }
    return null;
  },

  // Optional HttpResource options
  options: {
    reloadOn: [’submitted’] // Only revalidate on form submit
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Schema Composition
&lt;/h3&gt;

&lt;h4&gt;
  
  
  apply(path, schema)
&lt;/h4&gt;

&lt;p&gt;Applies a schema to a specific field path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const addressSchema = schema&amp;lt;Address&amp;gt;((path) =&amp;gt; {
  required(path.street);
  required(path.city);
});

form(data, (path) =&amp;gt; {
  apply(path.address, addressSchema);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  applyEach(path, schema)
&lt;/h4&gt;

&lt;p&gt;Applies a schema to each item in an array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const itemSchema = schema&amp;lt;Item&amp;gt;((path) =&amp;gt; {
  required(path.name);
  min(path.quantity, 1);
});

form(data, (path) =&amp;gt; {
  applyEach(path.items, itemSchema);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  applyWhen(path, condition, schema)
&lt;/h4&gt;

&lt;p&gt;Conditionally applies a schema based on form state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Only validate shipping address if different from billing
applyWhen(
  path.shippingAddress,
  (ctx) =&amp;gt; !ctx.valueOf(path.sameAsBilling),
  addressSchema
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  applyWhenValue(path, predicate, schema)
&lt;/h4&gt;

&lt;p&gt;Conditionally applies a schema based on field value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type PaymentMethod = 
  | { type: ‘card’; cardNumber: string; cvv: string }
  | { type: ‘paypal’; email: string }
  | { type: ‘bank’; accountNumber: string; routingNumber: string };

// Type-safe conditional schemas
applyWhenValue(
  path.payment,
  (payment): payment is Extract&amp;lt;PaymentMethod, { type: 'card' }&amp;gt; =&amp;gt; 
    payment.type === 'card',
  (cardPath) =&amp;gt; {
    required(cardPath.cardNumber);
    minLength(cardPath.cardNumber, 16);
    maxLength(cardPath.cardNumber, 16);
    required(cardPath.cvv);
    pattern(cardPath.cvv, /^\d{3,4}$/);
  }
);
applyWhenValue(
  path.payment,
  (payment): payment is Extract&amp;lt;PaymentMethod, { type: 'paypal' }&amp;gt; =&amp;gt; 
    payment.type === 'paypal',
  (paypalPath) =&amp;gt; {
    required(paypalPath.email);
    email(paypalPath.email);
  }
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Field State Logic
&lt;/h3&gt;

&lt;h4&gt;
  
  
  disabled(path, logic?)
&lt;/h4&gt;

&lt;p&gt;Makes a field disabled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;disabled(path.endDate, (ctx) =&amp;gt; !ctx.valueOf(path.hasEndDate));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  readonly(path, logic?)
&lt;/h4&gt;

&lt;p&gt;Makes a field readonly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;readonly(path.id); // Always readonly
readonly(path.price, (ctx) =&amp;gt; ctx.valueOf(path.isLocked));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  hidden(path, logic)
&lt;/h4&gt;

&lt;p&gt;Hides a field from display and validation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hidden(path.optionalDetails, (ctx) =&amp;gt; !ctx.valueOf(path.showDetails));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Form Submission
&lt;/h3&gt;

&lt;h4&gt;
  
  
  submit(form, action)
&lt;/h4&gt;

&lt;p&gt;Handles form submission with automatic validation and error handling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const onSubmit = submit(myForm, async (form) =&amp;gt; {
  try {
    await saveData(form().value());
    return null; // Success
  } catch (error) {
    return [{
      kind: ‘save_error’,
      message: ‘Failed to save’,
      field: form
    }];
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Template usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form (ngSubmit)=”onSubmit()”&amp;gt;
  &amp;lt;!-- form fields --&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Validation Errors
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Creating Errors
&lt;/h4&gt;

&lt;p&gt;Signal Forms provides type-safe error creation functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Built-in errors
requiredError({ message: ‘This field is required’ })
minError(10, { message: ‘Must be at least 10’ })
maxError(100, { message: ‘Cannot exceed 100’ })
minLengthError(5, { message: ‘Too short’ })
maxLengthError(50, { message: ‘Too long’ })
patternError(/\d+/, { message: ‘Must contain numbers’ })
emailError({ message: ‘Invalid email format’ })

// Custom errors
customError({ 
  kind: 'my_validation',
  message: 'Custom validation failed',
  additionalData: 'any value'
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Error Types
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface ValidationError {
  kind: string; // Error identifier
  field: Field&amp;lt;unknown&amp;gt;; // Target field
  message?: string; // User-facing message
}

// Specific error types
interface RequiredValidationError extends ValidationError {
  kind: 'required';
}
interface MinValidationError extends ValidationError {
  kind: 'min';
  min: number;
}

// Check error type
if (error instanceof NgValidationError) {
  switch (error.kind) {
    case 'required': /* ... */
    case 'min': /* ... */
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom Controls (no longer need ControlValueAccessor)
&lt;/h3&gt;

&lt;p&gt;To create custom form controls, implement FormValueControl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  selector: ‘app-custom-input’,
  template: `&amp;lt;div&amp;gt;Custom control&amp;lt;/div&amp;gt;`
})
export class CustomInputComponent implements FormValueControl&amp;lt;string&amp;gt; {
  value = model(’‘); // Required
  disabled = input(false); // Optional
  errors = input&amp;lt;ValidationError[]&amp;gt;([]); // Optional
  readonly = input(false); // Optional
  touched = model(false); // Optional
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;app-custom-input [control]=”myForm.field” /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Aggregate Properties
&lt;/h3&gt;

&lt;p&gt;Aggregate properties allow validators to contribute metadata to fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Read built-in properties
myForm.field().property(REQUIRED); // boolean
myForm.field().property(MIN_LENGTH); // number | undefined
myForm.field().property(MAX); // number | undefined
myForm.field().property(PATTERN); // RegExp[]

// Create custom properties
const TOOLTIP = createProperty&amp;lt;string&amp;gt;();
property(path.field, TOOLTIP, () =&amp;gt; 'Help text here');
// Read in template
{{ myForm.field().property(TOOLTIP) }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Type Safety
&lt;/h3&gt;

&lt;p&gt;Signal Forms maintain full TypeScript type inference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type User = {
  profile: {
    name: string;
    age: number;
  };
  tags: string[];
};

const userForm = form(signal&amp;lt;User&amp;gt;(...));
userForm.profile.name // Field&amp;lt;string&amp;gt;
userForm.profile.age // Field&amp;lt;number&amp;gt;
userForm.tags // Field&amp;lt;string[]&amp;gt; &amp;amp; Iterable
userForm.tags[0] // Field&amp;lt;string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This API overview provides the foundation for understanding how Signal Forms work. The next section will show how to migrate from reactive forms using these APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter Signal Forms: A New Paradigm
&lt;/h3&gt;

&lt;p&gt;Signal Forms treats your data model as the single source of truth, with forms being a reactive view of that model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Philosophy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Traditional Reactive Forms: Form manages state
const form = this.formBuilder.group({
  name: [’‘, Validators.required]
});

// Signal Forms: Data model is the source of truth
const user = signal({ name: '' });
const userForm = form(user); // Form reflects the signal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step-by-Step Migration Guide — &lt;a href="https://github.com/amosISA/angular-signal-forms/tree/feature/migration-to-signal-forms" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Let’s transform our weather chatbot from reactive forms to Signal Forms.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Define the Data Model
&lt;/h4&gt;

&lt;p&gt;First, we establish our data model as the source of truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// weather-chatbot-signal.component.ts
import { Component, signal, computed, inject, ChangeDetectionStrategy } from ‘@angular/core’;
import { form, Control, required, minLength, submit } from ‘@angular/forms/signals’;
import { CommonModule } from ‘@angular/common’;

type WeatherFormData = {
  date: string;
  country: string;
  city: string;
  temperatureUnit: 'celsius' | 'fahrenheit';
};
@Component({
  selector: 'app-weather-chatbot-signal',
  imports: [CommonModule, Control], // Note: Control instead of ReactiveFormsModule
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `...` // We'll update this next
})
export class WeatherChatbotSignalComponent {
  private readonly _chatService = inject(ChatService);
  // Step 1: Data model as source of truth
  private readonly _weatherData = signal&amp;lt;WeatherFormData&amp;gt;({
    date: new Date().toISOString().split('T')[0],
    country: '',
    city: '',
    temperatureUnit: 'celsius'
  });
  // Step 2: Create Signal Form
  protected readonly weatherForm = form(this._weatherData, (path) =&amp;gt; {
    required(path.date, { message: 'Date is required' });
    required(path.country, { message: 'Country is required' });
    required(path.city, { message: 'City is required' });
    minLength(path.country, 2, { message: 'Country must be at least 2 characters' });
    minLength(path.city, 2, { message: 'City must be at least 2 characters' });
    required(path.temperatureUnit, { message: 'Temperature unit is required' });
  });
  // Other signals remain the same
  protected readonly messages = signal&amp;lt;ChatMessage[]&amp;gt;([]);
  protected readonly isSubmitting = signal(false);
  protected readonly messageCount = computed(() =&amp;gt; this.messages().length);
  protected shouldShowErrors(fieldErrors: any[], fieldTouched: boolean): boolean {
    return fieldErrors.length &amp;gt; 0 &amp;amp;&amp;amp; fieldTouched;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The template becomes dramatically simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- Signal Forms Template --&amp;gt;
&amp;lt;form class=”space-y-4”&amp;gt;
  &amp;lt;!-- Date Input - Clean and Simple --&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;label for=”date” class=”block text-sm font-medium text-gray-700 mb-1”&amp;gt;
      Date
    &amp;lt;/label&amp;gt;
    &amp;lt;input
      id=”date”
      type=”date”
      [control]=”weatherForm.date”
      class=”w-full px-3 py-2 border border-gray-300 rounded-lg...”
    /&amp;gt;
     @if (shouldShowErrors(weatherForm.city().errors(), weatherForm.city().touched())) {
       @for (error of weatherForm.city().errors(); track $index) {
         &amp;lt;p class=”text-red-500 text-xs mt-1”&amp;gt;{{ error.message || ‘City is invalid’ }}&amp;lt;/p&amp;gt;
        }
     }
  &amp;lt;/div&amp;gt;

&amp;lt;!-- Same for other fields... --&amp;gt;
  &amp;lt;button
    type="button"
    (click)="onSubmitWeatherQuery()"
    [disabled]="!weatherForm().valid() || isSubmitting()"
    class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg..."
  &amp;gt;
    @if (isSubmitting()) {
      &amp;lt;span class="flex items-center justify-center"&amp;gt;
        &amp;lt;svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" ...&amp;gt;
          &amp;lt;!-- Loading spinner --&amp;gt;
        &amp;lt;/svg&amp;gt;
        Getting Weather...
      &amp;lt;/span&amp;gt;
    } @else {
      🌤️ Ask About Weather
    }
  &amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Update Form Submission
&lt;/h4&gt;

&lt;p&gt;You can do it by a custom function (button type) or with the submit feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// With type="button" and (click)="onSubmitWeatherQuery()"
protected onSubmitWeatherQuery(): void {
  if (!this.weatherForm().valid()) {
    this._markAllFieldsAsTouched();
    return;
  }

const formData = this._weatherData();
  const query = this._buildWeatherQuery(formData);
  this._addUserMessage(query);
  this._sendMessageToAI(query);
}
private _markAllFieldsAsTouched(): void {
  this.weatherForm.date().markAsTouched();
  this.weatherForm.country().markAsTouched();
  this.weatherForm.city().markAsTouched();
  this.weatherForm.temperatureUnit().markAsTouched();
}

// With (submit)="onSubmit($event)" and type="submit" on the button
protected readonly onSubmit = submit(this.weatherForm, (data) =&amp;gt; {
  const query = this._buildWeatherQuery(data);
  this._addUserMessage(query);
  this._sendMessageToAI(query);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dynamic Signal Form Arrays — &lt;a href="https://github.com/amosISA/angular-signal-forms/tree/feature/dynamic-form-arrays" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Real-world applications often require managing collections of data. In our Weather Assistant, we might want users to query multiple locations simultaneously. Signal Forms handles dynamic arrays elegantly through the applyEach function, which applies validation schemas to each array element.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx6glkzrtf7z2osc0cgpf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx6glkzrtf7z2osc0cgpf.png" alt="Dynamic Signal Form Arrays" width="800" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementing Multi-Location Support&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s extend our weather application to support multiple locations. First, we’ll refactor the data model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updated Type Definitions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type WeatherLocation = {
  city: string;
  country: string;
};

type WeatherFormData = {
  date: string;
  locations: WeatherLocation[]; // Array instead of single city/country
  temperatureUnit: TemperatureUnit;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Modified Component (weather-chatbot.component.ts):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  selector: 'app-weather-chatbot',
  templateUrl: ‘./weather-chatbot.component.html’,
  imports: [CommonModule, Control, JsonPipe],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WeatherChatbotComponent {
  // Update data model to use locations array
  private readonly _weatherData = signal&amp;lt;WeatherFormData&amp;gt;({
    date: new Date().toISOString().split(’T’)[0],
    locations: [{ city: ‘’, country: ‘’ }], // Start with one location
    temperatureUnit: ‘celsius’,
  });

  // Apply validation to each location using applyEach
  protected readonly weatherForm = form(this._weatherData, (path) =&amp;gt; {
    required(path.date, { message: ‘Date is required’ });

    // The key change: applyEach creates a schema for each array element
    applyEach(path.locations, (location) =&amp;gt; {
      // ‘location’ is PathKind.Item - represents one element
      required(location.city, { message: ‘City is required’ });
      minLength(location.city, 2, { message: ‘City must be at least 2 characters’ });
      required(location.country, { message: ‘Country is required’ });
      minLength(location.country, 2, { message: ‘Country must be at least 2 characters’ });
    });

    required(path.temperatureUnit, { message: ‘Temperature unit is required’ });
  });

  // Add new location to the array
  protected addLocation(): void {
    this._weatherData.update(data =&amp;gt; ({
      ...data,
      locations: [...data.locations, { city: ‘’, country: ‘’ }]
    }));
  }

  // Remove location by index
  protected removeLocation(index: number): void {
    this._weatherData.update(data =&amp;gt; ({
      ...data,
      locations: data.locations.filter((_, i) =&amp;gt; i !== index)
    }));
  }

  // Update to mark all location fields as touched
  private _markAllFieldsAsTouched(): void {
    this.weatherForm.date().markAsTouched();
    this.weatherForm.temperatureUnit().markAsTouched();

    // Iterate through array fields
    for (const location of this.weatherForm.locations) {
      location.city().markAsTouched();
      location.country().markAsTouched();
    }
  }

  // Update query builder to handle multiple locations
  private _buildWeatherQuery(data: WeatherFormData): string {
    const date = new Date(data.date).toLocaleDateString(’en-US’, {
      weekday: ‘long’,
      year: ‘numeric’,
      month: ‘long’,
      day: ‘numeric’,
    });

    const unit = data.temperatureUnit === ‘celsius’ ? ‘°C’ : ‘°F’;

    // Format multiple locations
    const locationsList = data.locations
      .map(loc =&amp;gt; `${loc.city}, ${loc.country}`)
      .join(’ and ‘);

    return `What’s the weather forecast for ${locationsList} on ${date}? Please provide the temperature in ${unit}.`;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Template Changes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The template uses Angular’s &lt;a class="mentioned-user" href="https://dev.to/for"&gt;@for&lt;/a&gt; to iterate over the locations array. Each location gets its own set of fields bound to the corresponding array element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- Locations Array --&amp;gt;
&amp;lt;div class=”space-y-3”&amp;gt;
  &amp;lt;label class=”block text-sm font-medium text-gray-700”&amp;gt;Locations&amp;lt;/label&amp;gt;

  @for (location of weatherForm.locations; track $index; let i = $index) {
    &amp;lt;div class=”border border-gray-200 rounded-lg p-3 space-y-3”&amp;gt;
      &amp;lt;div class=”flex justify-between items-center”&amp;gt;
        &amp;lt;span class=”text-sm font-medium text-gray-600”&amp;gt;Location {{ i + 1 }}&amp;lt;/span&amp;gt;
        @if (weatherForm.locations.length &amp;gt; 1) {
          &amp;lt;button 
            type=”button”
            (click)=”removeLocation(i)”
            class=”text-red-600 hover:text-red-700 text-sm”
          &amp;gt;
            Remove
          &amp;lt;/button&amp;gt;
        }
      &amp;lt;/div&amp;gt;

      &amp;lt;!-- City Field --&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;label [attr.for]=”’city-’ + i” class=”block text-xs font-medium text-gray-600 mb-1”&amp;gt;
          City
        &amp;lt;/label&amp;gt;
        &amp;lt;input
          [id]=”’city-’ + i”
          type=”text”
          [control]=”weatherForm.locations[i].city”
          placeholder=”e.g., New York”
          class=”w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500”
        /&amp;gt;
        @if (shouldShowErrors(weatherForm.locations[i].city().errors(), 
                              weatherForm.locations[i].city().touched())) {
          @for (error of weatherForm.locations[i].city().errors(); track error) {
            &amp;lt;p class=”text-red-500 text-xs mt-1”&amp;gt;{{ error.message }}&amp;lt;/p&amp;gt;
          }
        }
      &amp;lt;/div&amp;gt;

      &amp;lt;!-- Country Field --&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;label [attr.for]=”’country-’ + i” class=”block text-xs font-medium text-gray-600 mb-1”&amp;gt;
          Country
        &amp;lt;/label&amp;gt;
        &amp;lt;input
          [id]=”’country-’ + i”
          type=”text”
          [control]=”weatherForm.locations[i].country”
          placeholder=”e.g., United States”
          class=”w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500”
        /&amp;gt;
        @if (shouldShowErrors(weatherForm.locations[i].country().errors(), 
                              weatherForm.locations[i].country().touched())) {
          @for (error of weatherForm.locations[i].country().errors(); track error) {
            &amp;lt;p class=”text-red-500 text-xs mt-1”&amp;gt;{{ error.message }}&amp;lt;/p&amp;gt;
          }
        }
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  }

  &amp;lt;button
    type=”button”
    (click)=”addLocation()”
    class=”w-full border-2 border-dashed border-gray-300 rounded-lg p-3 text-gray-600 hover:border-blue-500 hover:text-blue-600 text-sm”
  &amp;gt;
    + Add Another Location
  &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Real-World Weather API Example — &lt;a href="https://github.com/amosISA/angular-signal-forms/tree/feature/validations" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74kmnjbirrxcgy7m3ido.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74kmnjbirrxcgy7m3ido.png" alt="Angular Signal Forms: validateAsync" width="800" height="258"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Validate city exists using weather API
validateAsync(location.city, {
        params: (ctx) =&amp;gt; {
          const city = ctx.value();
          const country = ctx.fieldOf(location.country)().value();

          if (!city || city.length &amp;lt; 2 || !country || country.length &amp;lt; 2) {
            return undefined;
          }

          return { city, country };
        },

        factory: (params) =&amp;gt; {
          return rxResource({
            params,
            stream: (p) =&amp;gt; {
              if (!p.params) return of(null);

              const { city, country } = p.params;
              const cacheKey = this._getCacheKey(city, country);

              // Check cache first
              if (this._cityValidationCache.has(cacheKey)) {
                console.log(`Using cached result for ${cacheKey}`);
                return of(this._cityValidationCache.get(cacheKey));
              }

              const apiKey = this._config.get(’WEATHER_API_KEY’);
              const url = `https://api.weatherapi.com/v1/search.json?key=${apiKey}&amp;amp;q=${encodeURIComponent(
                city
              )},${encodeURIComponent(country)}`;

              return of(null).pipe(
                delay(2000),
                switchMap(() =&amp;gt; this._http.get(url)),
                tap((results) =&amp;gt; {
                  // Store in cache after successful fetch
                  this._cityValidationCache.set(cacheKey, results);
                })
              );
            },
          });
        },
        errors: (results, ctx) =&amp;gt; {
          console.log(results);
          if (!results || results.length === 0) {
            return customError({
              kind: ‘city_not_found’,
              message: `Could not find “${ctx.value()}” in weather database`,
            });
          }

          const exactMatch = results.some(
            (r: any) =&amp;gt;
              r.name.toLowerCase() === ctx.value().toLowerCase() &amp;amp;&amp;amp;
              r.country.toLowerCase() === ctx.fieldOf(location.country)().value().toLowerCase()
          );

          if (!exactMatch) {
            return customError({
              kind: ‘city_country_mismatch’,
              message: `”${ctx.value()}” does not exist in ${ctx
                .fieldOf(location.country)()
                .value()}`,
            });
          }
          return null;
        },
      });

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Async Validator Behavior
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Only runs after &lt;strong&gt;all sync validators pass&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Field shows pending() === true while async validation runs&lt;/li&gt;
&lt;li&gt;Updates automatically when dependencies change&lt;/li&gt;
&lt;li&gt;Can be debounced or throttled using resource options
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- Show pending state --&amp;gt;
@if (weatherForm.city().pending()) {
  &amp;lt;span class=”text-blue-500 text-xs”&amp;gt;Verifying city...&amp;lt;/span&amp;gt;
}

@if (weatherForm.city().errors().length &amp;gt; 0) {
  @for (error of weatherForm.city().errors(); track error) {
    &amp;lt;p class="text-red-500 text-xs"&amp;gt;{{ error.message }}&amp;lt;/p&amp;gt;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cross-Field Validation with validate() - &lt;a href="https://github.com/amosISA/angular-signal-forms/tree/feature/validations" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Validate relationships between sibling fields by validating a parent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate(path, (ctx) =&amp;gt; {
  const locations = ctx.value().locations;

  if (locations.length === 2) {
    const [first, second] = locations;
    if (first.city === second.city &amp;amp;&amp;amp; first.country === second.country) {
      return customError({
        kind: ‘same_locations’,
        message: ‘Locations must be different’
      });
    }
  }

  return null;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tree Validators with validateTree() — &lt;a href="https://github.com/amosISA/angular-signal-forms/tree/feature/validations" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;While validate() works well for single-field validation and simple cross-field checks, it has a critical limitation: &lt;strong&gt;errors can only be assigned to the field being validated&lt;/strong&gt;. When you need to validate relationships across multiple fields and target errors to specific locations in your form tree, validateTree() is the solution.&lt;/p&gt;

&lt;p&gt;With validate(), you can detect duplicates, but the error appears on the parent field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Using validate() - error shows on the root form, not specific fields
validate(path, (ctx) =&amp;gt; {
  const locations = ctx.value().locations;
  // ... duplicate detection logic
  return customError({
    kind: ‘duplicate_location’,
    message: ‘You have duplicate locations’
    // Error appears on the form root, not helpful for users
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With validateTree(), you can target errors to the exact duplicate fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Using validateTree() - errors appear on duplicate city fields
validateTree(path, (ctx) =&amp;gt; {
  const errors: any[] = [];
  const locations = ctx.value().locations;

  locations.forEach((location, index) =&amp;gt; {
    const city = location.city.valueOf();
    const country = location.country.valueOf();

    if (!city || !country) return; // Skip empty values

    locations.forEach((otherLocation, otherIndex) =&amp;gt; {
      if (index !== otherIndex) {
        if (
          city === otherLocation.city.valueOf() &amp;amp;&amp;amp;
          country === otherLocation.country.valueOf()
        ) {
          errors.push({
            kind: ‘duplicate_location’,
            field: ctx.field.locations[index].city, // Target specific field!
            message: `Duplicate location: ${city}, ${country}`,
          });
        }
      }
    });
  });

  return errors.length &amp;gt; 0 ? errors : null;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Standard Schema Integration with Zod — &lt;a href="https://github.com/amosISA/angular-signal-forms/tree/feature/standard-zod-validation" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;As applications grow, maintaining consistent validation rules across client and server becomes challenging. Signal Forms addresses this with validateStandardSchema(), allowing integration with popular schema validation libraries like Zod, Yup, and Valibot. This section demonstrates Zod integration in our weather application.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Standard Schema Validation?
&lt;/h4&gt;

&lt;p&gt;While Signal Forms’ built-in validators are powerful, standard schema libraries offer several advantages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single Source of Truth&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Define once, use everywhere
const weatherFormSchema = z.object({
  date: z.string().min(1),
  city: z.string().min(2).max(50),
  // ... share between client and server
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Type Inference&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// TypeScript types automatically generated from schema
type WeatherFormData = z.infer&amp;lt;typeof weatherFormSchema&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Setting Up Zod Schemas
&lt;/h4&gt;

&lt;p&gt;First, install Zod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install zod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a dedicated file for your schemas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;weather-form.schemas.ts&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from ‘zod’;

export const weatherLocationSchema = z.object({
  city: z.string()
    .min(2, 'City must be at least 2 characters')
    .max(50, 'City name is too long'),
  country: z.string()
    .min(2, 'Country must be at least 2 characters')
    .max(50, 'Country name is too long')
});
export const weatherFormSchema = z.object({
  date: z.string()
    .min(1, 'Date is required')
    .refine((date) =&amp;gt; {
      const selectedDate = new Date(date);
      const today = new Date();
      today.setHours(0, 0, 0, 0);
      return selectedDate &amp;gt;= today;
    }, {
      message: 'Date cannot be in the past'
    }),
  locations: z.array(weatherLocationSchema)
    .min(1, 'At least one location is required')
    .max(5, 'Maximum 5 locations allowed'),
  temperatureUnit: z.enum(['celsius', 'fahrenheit'], {
    errorMap: () =&amp;gt; ({ message: 'Temperature unit is required' })
  })
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Integration with Signal Forms (Hybrid Approach with Zod)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { validateStandardSchema } from ‘@angular/forms/signals’;
import { weatherFormSchema } from ‘./weather-form.schemas’;

protected readonly weatherForm = form(this._weatherData, (path) =&amp;gt; {
  // Single line replaces all basic validation!
  validateStandardSchema(path, weatherFormSchema);

  // Keep custom validators for Angular-specific logic
  applyEach(path.locations, (location) =&amp;gt; {
    // Async validation for API calls
    validateAsync(location.city, {
      params: (ctx) =&amp;gt; {
        const city = ctx.value();
        const country = ctx.fieldOf(location.country)().value();
        if (!city || city.length &amp;lt; 2 || !country || country.length &amp;lt; 2) {
          return undefined;
        }
        return { city, country };
      },
      factory: (params) =&amp;gt; {
        return rxResource({
          params,
          stream: (p) =&amp;gt; {
            if (!p.params) return of(null);
            const { city, country } = p.params;
            const apiKey = this._config.get('WEATHER_API_KEY');
            const url = `https://api.weatherapi.com/v1/search.json?key=${apiKey}&amp;amp;q=${city},${country}`;
            return this._http.get(url);
          },
        });
      },
      errors: (results, ctx) =&amp;gt; {
        if (!results || results.length === 0) {
          return customError({
            kind: 'city_not_found',
            message: `Could not find "${ctx.value()}" in weather database`,
          });
        }
        return null;
      },
    });
  });
  // Tree validation for complex cross-field logic
  validateTree(path, (ctx) =&amp;gt; {
    const errors: any[] = [];
    const locations = ctx.value().locations;
    locations.forEach((location, index) =&amp;gt; {
      const city = location.city.valueOf();
      const country = location.country.valueOf();
      if (!city || !country) return;
      locations.forEach((otherLocation, otherIndex) =&amp;gt; {
        if (index !== otherIndex) {
          if (
            city === otherLocation.city.valueOf() &amp;amp;&amp;amp;
            country === otherLocation.country.valueOf()
          ) {
            errors.push({
              kind: 'duplicate_location',
              field: ctx.field.locations[index].city,
              message: `Duplicate location: ${city}, ${country}`,
            });
          }
        }
      });
    });
    return errors.length &amp;gt; 0 ? errors : null;
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The integration between Signal Forms and standard schema libraries like Zod demonstrates Angular’s commitment to interoperability and developer choice, allowing you to build robust forms using the tools you prefer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema Functions: Building Reusable Form Logic — &lt;a href="https://github.com/amosISA/angular-signal-forms/tree/feature/schemas" rel="noopener noreferrer"&gt;Source Code&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;A schema is a &lt;strong&gt;reusable validation blueprint&lt;/strong&gt; that encapsulates all the rules, logic, and constraints for a particular data structure. Think of it as a template that can be applied to any compatible field in your form tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline Schema vs. Schema Function&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Inline: Validation defined directly in the form
const myForm = form(signal(data), (path) =&amp;gt; {
  required(path.city);
  minLength(path.city, 2);
  required(path.country);
  minLength(path.country, 2);
});

// Schema Function: Reusable validation logic
const locationSchema = schema&amp;lt;WeatherLocation&amp;gt;((path) =&amp;gt; {
  required(path.city);
  minLength(path.city, 2);
  required(path.country);
  minLength(path.country, 2);
});
// Apply the schema anywhere
const myForm = form(signal(data), (path) =&amp;gt; {
  apply(path, locationSchema);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Creating Basic Schemas
&lt;/h4&gt;

&lt;p&gt;Let’s build schemas for our weather application, starting simple and progressing to complex patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple Field Schema&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { schema, required, minLength, maxLength } from ‘@angular/forms/signals’;

// Schema for a single city name
const cityNameSchema = schema&amp;lt;string&amp;gt;((path) =&amp;gt; {
  required(path, { message: 'City is required' });
  minLength(path, 2, { message: 'City must be at least 2 characters' });
  maxLength(path, 50, { message: 'City name is too long' });
});

// Apply to a field
form(signal({ city: '' }), (path) =&amp;gt; {
  apply(path.city, cityNameSchema);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Object Schema&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type WeatherLocation = {
  city: string;
  country: string;
};

const locationSchema = schema&amp;lt;WeatherLocation&amp;gt;((path) =&amp;gt; {
  // Validate city field
  required(path.city, { message: 'City is required' });
  minLength(path.city, 2, { message: 'City must be at least 2 characters' });
  maxLength(path.city, 50, { message: 'City name is too long' });

  // Validate country field
  required(path.country, { message: 'Country is required' });
  minLength(path.country, 2, { message: 'Country must be at least 2 characters' });
  maxLength(path.country, 50, { message: 'Country name is too long' });
});

// Use it
form(signal&amp;lt;WeatherLocation&amp;gt;({ city: '', country: '' }), (path) =&amp;gt; {
  apply(path, locationSchema);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Schema Composition: Building Complex Schemas from Simple Ones
&lt;/h4&gt;

&lt;p&gt;One of the most powerful features of schemas is composition — building complex validation from smaller, reusable pieces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// weather-form.schemas.ts
import { 
  schema, 
  required, 
  minLength, 
  maxLength, 
  validate,
  customError,
  apply,
  applyEach
} from ‘@angular/forms/signals’;

// 1. Atomic schemas - smallest reusable units
const cityNameSchema = schema&amp;lt;string&amp;gt;((path) =&amp;gt; {
  required(path, { message: ‘City is required’ });
  minLength(path, 2, { message: ‘City must be at least 2 characters’ });
  maxLength(path, 50, { message: ‘City name is too long’ });
});

const countryNameSchema = schema&amp;lt;string&amp;gt;((path) =&amp;gt; {
  required(path, { message: ‘Country is required’ });
  minLength(path, 2, { message: ‘Country must be at least 2 characters’ });
  maxLength(path, 50, { message: ‘Country name is too long’ });
});

// 2. Composite schema - combines atomic schemas
export const locationSchema = schema&amp;lt;WeatherLocation&amp;gt;((path) =&amp;gt; {
  apply(path.city, cityNameSchema);
  apply(path.country, countryNameSchema);
});

// 3. Array schema with composite validation
export const locationsArraySchema = schema&amp;lt;WeatherLocation[]&amp;gt;((path) =&amp;gt; {
  // Validate array itself
  validate(path, (ctx) =&amp;gt; {
    if (ctx.value().length === 0) {
      return customError({
        kind: ‘empty_array’,
        message: ‘At least one location is required’
      });
    }
    if (ctx.value().length &amp;gt; 5) {
      return customError({
        kind: ‘too_many’,
        message: ‘Maximum 5 locations allowed’
      });
    }
    return null;
  });

  // Apply location schema to each item
  applyEach(path, locationSchema);
});

// 4. Date validation schema
export const futureDateSchema = schema&amp;lt;string&amp;gt;((path) =&amp;gt; {
  required(path, { message: ‘Date is required’ });

  validate(path, (ctx) =&amp;gt; {
    const selectedDate = new Date(ctx.value());
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    if (selectedDate &amp;lt; today) {
      return customError({
        kind: ‘past_date’,
        message: ‘Date cannot be in the past’
      });
    }

    const maxDate = new Date();
    maxDate.setDate(maxDate.getDate() + 14);

    if (selectedDate &amp;gt; maxDate) {
      return customError({
        kind: ‘far_future’,
        message: ‘Weather forecasts only available for the next 14 days’
      });
    }

    return null;
  });
});

// 5. Temperature unit schema
export const temperatureUnitSchema = schema&amp;lt;TemperatureUnit&amp;gt;((path) =&amp;gt; {
  required(path, { message: ‘Temperature unit is required’ });

  validate(path, (ctx) =&amp;gt; {
    const value = ctx.value();
    if (value !== ‘celsius’ &amp;amp;&amp;amp; value !== ‘fahrenheit’) {
      return customError({
        kind: ‘invalid_unit’,
        message: ‘Temperature unit must be celsius or fahrenheit’
      });
    }
    return null;
  });
});

// 6. Complete form schema - orchestrates all schemas
export const weatherFormSchema = schema&amp;lt;WeatherFormData&amp;gt;((path) =&amp;gt; {
  apply(path.date, futureDateSchema);
  apply(path.locations, locationsArraySchema);
  apply(path.temperatureUnit, temperatureUnitSchema);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Using Schemas in Your Component
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Approach 1: Apply Complete Schema&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  selector: ‘app-weather-chatbot’,
  // ...
})
export class WeatherChatbotComponent {
  private readonly _weatherData = signal&amp;lt;WeatherFormData&amp;gt;({
    date: new Date().toISOString().split(’T’)[0],
    locations: [{ city: ‘’, country: ‘’ }],
    temperatureUnit: ‘celsius’,
  });

  protected readonly weatherForm = form(this._weatherData, (path) =&amp;gt; {
    // Single line applies all validation
    apply(path, weatherFormSchema);

    // Add custom validators on top
    applyEach(path.locations, (location) =&amp;gt; {
      validateAsync(location.city, {
        // ... async validation
      });
    });

    validateTree(path, (ctx) =&amp;gt; {
      // ... duplicate detection
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Approach 2: Apply Partial Schemas&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;protected readonly weatherForm = form(this._weatherData, (path) =&amp;gt; {
  // Pick and choose which schemas to apply
  apply(path.date, futureDateSchema);
  apply(path.locations, locationsArraySchema);
  apply(path.temperatureUnit, temperatureUnitSchema);

  // Add inline validation for specific needs
  validate(path, (ctx) =&amp;gt; {
    // Custom form-level validation
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Approach 3: Conditional Schema Application&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;protected readonly weatherForm = form(this._weatherData, (path) =&amp;gt; {
  apply(path.date, futureDateSchema);

  // Apply different validation based on mode
  if (this.isPremiumUser()) {
    // Premium users can add more locations
    apply(path.locations, premiumLocationsArraySchema);
  } else {
    apply(path.locations, locationsArraySchema);
  }

  apply(path.temperatureUnit, temperatureUnitSchema);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Advanced Schema Patterns
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Parametric Schemas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create schemas that accept configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Schema factory that accepts parameters
function createLocationLimitSchema(min: number, max: number) {
  return schema&amp;lt;WeatherLocation[]&amp;gt;((path) =&amp;gt; {
    validate(path, (ctx) =&amp;gt; {
      const length = ctx.value().length;

      if (length &amp;lt; min) {
        return customError({
          kind: ‘too_few’,
          message: `At least ${min} location${min &amp;gt; 1 ? ‘s’ : ‘’} required`
        });
      }

      if (length &amp;gt; max) {
        return customError({
          kind: ‘too_many’,
          message: `Maximum ${max} locations allowed`
        });
      }

      return null;
    });

    applyEach(path, locationSchema);
  });
}

// Use with different limits
const freeUserForm = form(data, (path) =&amp;gt; {
  apply(path.locations, createLocationLimitSchema(1, 3));
});

const premiumUserForm = form(data, (path) =&amp;gt; {
  apply(path.locations, createLocationLimitSchema(1, 10));
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conditional Validation Schemas&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type WeatherQuery = {
  searchType: ‘current’ | ‘forecast’;
  date?: string;
  locations: WeatherLocation[];
};

const currentWeatherSchema = schema&amp;lt;WeatherQuery&amp;gt;((path) =&amp;gt; {
  apply(path.locations, locationsArraySchema);

  // Date should be hidden/ignored for current weather
  hidden(path.date);
});

const forecastWeatherSchema = schema&amp;lt;WeatherQuery&amp;gt;((path) =&amp;gt; {
  apply(path.locations, locationsArraySchema);
  apply(path.date, futureDateSchema);
});

// Apply conditionally
protected readonly weatherForm = form(this._weatherData, (path) =&amp;gt; {
  applyWhenValue(
    path,
    (value): value is Extract&amp;lt;WeatherQuery, { searchType: ‘current’ }&amp;gt; =&amp;gt;
      value.searchType === ‘current’,
    currentWeatherSchema
  );

  applyWhenValue(
    path,
    (value): value is Extract&amp;lt;WeatherQuery, { searchType: ‘forecast’ }&amp;gt; =&amp;gt;
      value.searchType === ‘forecast’,
    forecastWeatherSchema
  );
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Schema vs. Inline: When to Use Each
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Use Schemas When:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validation logic is shared across multiple forms&lt;/li&gt;
&lt;li&gt;Testing validation logic independently&lt;/li&gt;
&lt;li&gt;Building a validation library for your organization&lt;/li&gt;
&lt;li&gt;Complex nested structures benefit from composition&lt;/li&gt;
&lt;li&gt;Team needs clear documentation of validation rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Inline When:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validation is unique to a single form&lt;/li&gt;
&lt;li&gt;Quick prototyping or simple forms&lt;/li&gt;
&lt;li&gt;Validation tightly coupled to component state&lt;/li&gt;
&lt;li&gt;One-off forms that won’t be reused&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schemas transform Signal Forms from a validation tool into a scalable validation architecture, enabling teams to build maintainable, testable, and reusable form logic across their entire application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Signal Forms is currently experimental in Angular 21.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Throughout this guide, we’ve transformed a real-world weather chatbot application, demonstrating how Signal Forms addresses pain points at every level:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reduced Complexity&lt;/strong&gt; : What once required FormBuilder, FormGroup, and manual subscription management now distills into a single form() function bound to a signal. The verbose template logic with repeated weatherForm.get() calls becomes clean, type-safe field navigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enhanced Validation&lt;/strong&gt; : From basic built-in validators to async API validation with validateAsync(), cross-field logic with validateTree(), and standard schema integration with Zod—Signal Forms provides a complete validation toolkit that scales from simple forms to complex, multi-step workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composable Architecture&lt;/strong&gt; : The schema() function enables building reusable validation blueprints that can be tested independently, shared across teams, and composed into sophisticated validation hierarchies. This modularity transforms validation from scattered logic into maintainable, documented architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance by Default&lt;/strong&gt; : Signals provide fine-grained reactivity without Observable overhead. OnPush change detection works naturally, and the framework only updates what changed. The result is forms that are both easier to write and faster to run.&lt;/p&gt;

&lt;p&gt;The forms you’ll build with Signal Forms — once stable — will be simpler, faster, and more maintainable than ever before. The experimental phase is Angular’s invitation to help shape that future.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thanks for reading so far 🙏
&lt;/h3&gt;

&lt;p&gt;I’d like to have your feedback so please leave a &lt;strong&gt;comment&lt;/strong&gt; , &lt;strong&gt;clap&lt;/strong&gt; or &lt;strong&gt;follow&lt;/strong&gt;. &lt;em&gt;👏&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spread the Angular love! 💜&lt;/p&gt;

&lt;p&gt;If you really liked it, &lt;strong&gt;share it&lt;/strong&gt; among your community, tech bros and whoever you want! 🚀👥&lt;/p&gt;

&lt;p&gt;Don’t forget to follow me and stay updated: 📱&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://www.linkedin.com/in/amos-lucian-isaila-34ab78146/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;a href="https://codigotipado.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Newsletter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@codigotipado" rel="noopener noreferrer"&gt;&lt;strong&gt;YouTube&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;a href="https://twitter.com/amosisaila" rel="noopener noreferrer"&gt;&lt;strong&gt;Twitter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for being part of this Angular journey! 👋😁&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a href="https://www.codigotipado.com/p/mastering-angular-21-signal-forms" rel="noopener noreferrer"&gt;&lt;em&gt;https://www.codigotipado.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular21</category>
      <category>httpvalidation</category>
      <category>validatetree</category>
      <category>reactiveforms</category>
    </item>
    <item>
      <title>Angular 20.2.0: What’s new</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Thu, 21 Aug 2025 04:50:01 +0000</pubDate>
      <link>https://forem.com/amosisaila/angular-2020-whats-new-3j23</link>
      <guid>https://forem.com/amosisaila/angular-2020-whats-new-3j23</guid>
      <description>&lt;h3&gt;
  
  
  Angular 20.2.0 introduces cleaner templates, smarter tooling, and improved debugging as some of its new features.
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6iw3npibifvfsm0j36wh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6iw3npibifvfsm0j36wh.png" alt="Angular 20.2.0" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. 🔥 @angular/core
&lt;/h3&gt;

&lt;h3&gt;
  
  
  a) Support TypeScript 5.9
&lt;/h3&gt;

&lt;h3&gt;
  
  
  b) Angular Components Now Use Real Tag Names in Tests — &lt;a href="https://github.com/angular/angular/pull/62283" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Until now, there was a subtle but important difference between how your components behaved in tests versus production:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Production:&lt;/strong&gt; angular creates components using tag names inferred from their selectors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({selector: 'user-profile', template: '...'})
// Creates actual &amp;lt;user-profile&amp;gt; element
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In Tests (Previously):&lt;/strong&gt; TestBed always wrapped components in generic &lt;/p&gt; elements:&lt;br&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const fixture = TestBed.createComponent(UserProfileComponent);
// Always created &amp;lt;div&amp;gt; regardless of selector
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This mismatch could hide CSS styling issues, accessibility problems, or any logic that depended on the actual element tag name.&lt;/p&gt;

&lt;p&gt;Angular now offers the inferTagName option to align test behavior with production:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Option 1: Per-component basis
const fixture = TestBed.createComponent(UserProfileComponent, {
  inferTagName: true
});
// Now creates &amp;lt;user-profile&amp;gt; element in tests

// Option 2: Configure globally for all tests
TestBed.configureTestingModule({
  inferTagName: true,
  // ... other config
});
@Component({selector: 'my-button'}) 
// → Creates &amp;lt;my-button&amp;gt;
@Component({selector: 'custom-input[type="text"]'}) 
// → Creates &amp;lt;custom-input&amp;gt;
@Component({selector: '[data-widget]'}) 
// → Falls back to &amp;lt;div&amp;gt; (no tag name in selector)
@Component({template: '...'}) 
// → Creates &amp;lt;ng-component&amp;gt; (no selector)
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  c) Property-to-Attribute Mapping — &lt;a href="https://github.com/angular/angular/pull/62630" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Previously, developers faced a choice between verbose syntax and potential SSR problems when working with ARIA attributes:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Verbose but SSR-safe
&amp;lt;button [attr.aria-label]="buttonText"&amp;gt;

// Clean but potential SSR issues  
&amp;lt;button [ariaLabel]="buttonText"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The problem: ARIA DOM properties don’t always correctly reflect as HTML attributes during server-side rendering, potentially breaking accessibility for users with assistive technologies.&lt;/p&gt;

&lt;p&gt;Angular now supports clean property binding syntax that automatically renders as proper HTML attributes:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// All of these now work identically and render correctly on SSR
&amp;lt;button [aria-label]="buttonText"&amp;gt; // New simplified syntax
&amp;lt;button [ariaLabel]="buttonText"&amp;gt; // Existing camelCase
&amp;lt;button [attr.aria-label]="buttonText"&amp;gt; // Explicit attribute binding
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The enhancement includes intelligent mapping for all standard ARIA properties:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  template: `
    &amp;lt;div [ariaLabel]="label"
         [ariaExpanded]="isExpanded" 
         [ariaDisabled]="isDisabled"
         [aria-hidden]="isHidden"&amp;gt;
      &amp;lt;!-- All render as proper aria-* attributes --&amp;gt;
    &amp;lt;/div&amp;gt;
  `
})
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Automatic conversions include:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ariaLabel → aria-label&lt;/li&gt;
&lt;li&gt;ariaExpanded → aria-expanded&lt;/li&gt;
&lt;li&gt;ariaHasPopup → aria-haspopup&lt;/li&gt;
&lt;li&gt;Plus 30+ other ARIA properties&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system intelligently prioritizes component inputs over attribute binding:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  selector: 'custom-button'
})
class CustomButton {
  @Input() ariaLabel!: string; // This takes precedence
}

// Binds to component input, not HTML attribute
&amp;lt;custom-button [ariaLabel]="text"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This feature particularly shines in SSR scenarios where proper attribute rendering is crucial for accessibility:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Server renders: &amp;lt;button aria-label="Save Document"&amp;gt;
// Client hydrates seamlessly with identical markup
&amp;lt;button [aria-label]="saveLabel"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  d) Promote zoneless to stable
&lt;/h3&gt;

&lt;p&gt;As of Angular v20.2, Zoneless (provideZonelessChangeDetection) Angular is now stable and includes improvements in error handling and server-side rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  e) Control Flow Enhancement: as Aliases in &lt;a class="mentioned-user" href="https://dev.to/else"&gt;@else&lt;/a&gt; if Blocks - &lt;a href="https://github.com/angular/angular/pull/63047" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;



&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- ✅ Enhanced: as works in @else if blocks --&amp;gt;
@if (user$ | async; as user) {
  &amp;lt;h1&amp;gt;Welcome, {{user.name}}&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;Role: {{user.role}}&amp;lt;/p&amp;gt;
} @else if (userRole$ | async; as role) {
  &amp;lt;!-- 🎉 Now we can use 'as' here too! --&amp;gt;
  &amp;lt;p&amp;gt;Loading user data for {{role}}...&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;Please wait while we fetch your {{role}} profile...&amp;lt;/p&amp;gt;
} @else {
  &amp;lt;p&amp;gt;Please log in&amp;lt;/p&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  2. 🔥 @angular/common/http
&lt;/h3&gt;

&lt;h3&gt;
  
  
  a) Add referrer &amp;amp; integrity support for fetch requests in httpResource — &lt;a href="https://github.com/angular/angular/pull/62461" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Currently, Angular’s httpResource does not expose the referrer and integrity options from the underlying Fetch API.&lt;/p&gt;

&lt;p&gt;Exposing these options would provide developers with finer control over the request’s &lt;strong&gt;referrer&lt;/strong&gt; and &lt;strong&gt;subresource integrity validation&lt;/strong&gt; , which are important for ensuring &lt;strong&gt;security&lt;/strong&gt; , &lt;strong&gt;privacy&lt;/strong&gt; , and &lt;strong&gt;trust&lt;/strong&gt; in critical resource fetching scenarios.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;httpResource(() =&amp;gt; ({
  url: '${CDN_DOMAIN}/assets/data.json',
  method: 'GET',
  referrer: 'no-referrer',
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GhEXAMPLEKEY='
}));
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  b) New redirected Property - &lt;a href="https://github.com/angular/angular/pull/62675" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Angular’s HttpClient now provides complete visibility into HTTP redirects with a new redirected property that aligns with the native Fetch API.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Injectable({
  providedIn: 'root'
})
export class ApiService {
  constructor(private http: HttpClient) {}

  getUserProfile(userId: string) {
    return this.http.get(`/api/users/${userId}`, { 
      observe: 'response' 
    }).pipe(
      tap(response =&amp;gt; {
        if (response.redirected) {
          console.log('User profile request was redirected');
          console.log('Original URL:', `/api/users/${userId}`);
          console.log('Final URL:', response.url);

          // Track redirect for analytics
          this.trackRedirect('user-profile', response.url);
        }
      }),
      map(response =&amp;gt; response.body)
    );
  }

  private trackRedirect(endpoint: string, finalUrl: string | null) {
    // Send redirect data to analytics service
    analytics.track('http_redirect', {
      endpoint,
      finalUrl,
      timestamp: new Date().toISOString()
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  3. 🔥 @angular/language-service
&lt;/h3&gt;

&lt;h3&gt;
  
  
  a) Angular Language Service Now Detects Deprecated APIs in Templates — &lt;a href="https://github.com/angular/angular/pull/62054" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;



&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export class LegacyComponent {
  /**
   * @deprecated Use newProp instead
   */
  @Input() oldProp: string;
}

&amp;lt;!-- Your IDE will now show a suggestion diagnostic here --&amp;gt;
&amp;lt;legacy-component [oldProp]="someValue"&amp;gt;&amp;lt;/legacy-component&amp;gt;
                   ~~~~~~~'oldProp' is deprecated. Use newProp instead
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  b) Auto-Import for Angular Attributes: No More Manual Directive Imports — &lt;a href="https://github.com/angular/angular/pull/62797" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When you use directive attributes in your templates, the IDE will now automatically suggest importing the directive for you.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- Type this: --&amp;gt;
&amp;lt;div appHighlight="yellow"&amp;gt;
     ↑ 
💡 Quick Fix: Import HighlightDirective from '@app/highlight'

// Automatically added to your component:
import { HighlightDirective } from '@app/highlight';
@Component({
  imports: [HighlightDirective], // ← Added automatically!
  template: `&amp;lt;div appHighlight="yellow"&amp;gt;Content&amp;lt;/div&amp;gt;`
})
export class MyComponent { }
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  4. 🔥 @angular/service-worker
&lt;/h3&gt;

&lt;h3&gt;
  
  
  a) Take Control of Service Worker Updates: Angular’s New updateViaCache Configuration Option — &lt;a href="https://github.com/angular/angular/pull/62721" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The new updateViaCache option allows you to specify exactly when the browser should check its HTTP cache when updating service workers or any scripts imported via importScripts(). This translates to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Improved Performance Control&lt;/strong&gt; : choose between 'imports', 'all', or 'none' to optimize your update strategy based on your application's specific needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better Development Experience&lt;/strong&gt; : gain more predictable behavior during development cycles, especially when testing service worker updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production Optimization&lt;/strong&gt; : fine-tune caching strategies for production deployments where update timing is critical.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const appConfig: ApplicationConfig = {
  providers: [
    provideServiceWorker('ngsw-worker.js', {
      enabled: !isDevMode(),
      updateViaCache: 'imports', // New cache control option
      registrationStrategy: 'registerWhenStable:30000',
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  b) New proactive storage monitoring system that prevents cache failures before they happen — &lt;a href="https://github.com/angular/angular/pull/62737" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The system monitors storage usage and alerts when capacity reaches 95% of the available quota:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private async detectStorageFull() {
  try {
    const estimate = await navigator.storage.estimate();
    const { quota, usage } = estimate;

    // Handle cases where quota or usage might be undefined
    if (typeof quota !== 'number' || typeof usage !== 'number') {
      return;
    }

    // Consider storage "full" if usage is &amp;gt;= 95% of quota
    // This provides a safety buffer before actual storage exhaustion
    const usagePercentage = (usage / quota) * 100;
    const isStorageFull = usagePercentage &amp;gt;= 95;

    if (isStorageFull) {
      this.debugHandler.log(
        'Storage is full or nearly full',
        `DataGroup(${this.config.name}@${this.config.version}).detectStorageFull()`,
      );
    }
  } catch {
    // Error estimating storage, possibly by unsupported browser.
  }
}
&lt;/code&gt;&lt;/pre&gt;



&lt;h4&gt;
  
  
  For PWA Applications
&lt;/h4&gt;



&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Example of how this helps PWAs maintain offline functionality
if (storageNearFull) {
  // Implement cleanup strategies
  // Prioritize critical resources
  // Notify user about storage constraints
}
&lt;/code&gt;&lt;/pre&gt;



&lt;h4&gt;
  
  
  For Content-Heavy Apps
&lt;/h4&gt;



&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Applications with large caching needs benefit from early warnings
if (storageApproachingLimit) {
  // Implement cache eviction policies
  // Compress cached data
  // Switch to selective caching strategies
}
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Developers now get clear indicators when storage issues occur:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Storage is full or nearly full -DataGroup(api@v1.2.3).detectStorageFull()
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  c) Real-Time Version Failure Notifications — &lt;a href="https://github.com/angular/angular/commit/6d011687ec1fa2b8f0211379bb98adc8e02f4e9a" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Previously, when a Service Worker version encountered critical failures, applications would experience degraded functionality without clear visibility into the root cause. The system introduces a new VersionFailedEvent that provides comprehensive failure information:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * An event emitted when a specific version of the app has encountered a critical failure
 * that prevents it from functioning correctly.
 */
export interface VersionFailedEvent {
  type: 'VERSION_FAILED';
  version: {hash: string; appData?: object};
  error: string;
}
&lt;/code&gt;&lt;/pre&gt;



&lt;h4&gt;
  
  
  Automatic Client Notification
&lt;/h4&gt;

&lt;p&gt;When a version fails, the Service Worker automatically notifies all affected clients. Applications can listen for version failures using the existing SwUpdate service:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  selector: 'app-root',
  template: `
    &amp;lt;div class="app"&amp;gt;
      @if (versionError()) {
        &amp;lt;div class="error-banner"&amp;gt;
          &amp;lt;h3&amp;gt;Application Update Issue&amp;lt;/h3&amp;gt;
          &amp;lt;p&amp;gt;{{versionError()}}&amp;lt;/p&amp;gt;
          &amp;lt;button (click)="handleVersionFailure()"&amp;gt;
            Refresh Application
          &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      }

      &amp;lt;router-outlet /&amp;gt;
    &amp;lt;/div&amp;gt;
  `,
  styles: [`
    .error-banner {
      background: #fee;
      border: 1px solid #fcc;
      border-radius: 4px;
      padding: 16px;
      margin: 8px;
      color: #c66;
    }

    .error-banner button {
      background: #c66;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
      margin-top: 8px;
    }
  `]
})
export class AppComponent implements OnInit {
  versionError = signal&amp;lt;string | null&amp;gt;(null);

  constructor(private swUpdate: SwUpdate) {}

  ngOnInit() {
    // Listen for all version events
    this.swUpdate.versionUpdates.subscribe(event =&amp;gt; {
      switch (event.type) {
        case 'VERSION_FAILED':
          this.handleVersionFailure(event);
          break;
        case 'VERSION_READY':
          this.handleVersionReady(event);
          break;
        case 'VERSION_DETECTED':
          this.handleVersionDetected(event);
          break;
      }
    });
  }

  private handleVersionFailure(event: VersionFailedEvent) {
    console.error('Service Worker version failed:', event);

    // Set user-friendly error message
    this.versionError.set(
      `Application version ${event.version.hash.substring(0, 8)} has encountered an error. ` +
      `Please refresh to restore full functionality.`
    );

    // Optional: Report to error monitoring service
    this.reportVersionFailure(event);
  }

  private handleVersionReady(event: VersionReadyEvent) {
    // Clear any previous errors when new version is ready
    this.versionError.set(null);
    console.log('New version ready:', event.latestVersion.hash);
  }

  private handleVersionDetected(event: VersionDetectedEvent) {
    console.log('New version detected:', event.latestVersion.hash);
  }

  handleVersionFailure() {
    // Force page reload to get latest version
    window.location.reload();
  }

  private reportVersionFailure(event: VersionFailedEvent) {
    // Example: Send to monitoring service
    // errorService.report({
    // type: 'service-worker-version-failure',
    // version: event.version.hash,
    // error: event.error,
    // userAgent: navigator.userAgent,
    // timestamp: new Date().toISOString()
    // });
  }
}
&lt;/code&gt;&lt;/pre&gt;



&lt;h4&gt;
  
  
  Testing the Feature
&lt;/h4&gt;

&lt;p&gt;The new functionality includes comprehensive test coverage:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;it('processes version failed events with cache corruption error', (done) =&amp;gt; {
  update.versionUpdates.subscribe((event) =&amp;gt; {
    expect(event.type).toEqual('VERSION_FAILED');
    expect((event as VersionFailedEvent).version).toEqual({
      hash: 'B',
      appData: {name: 'test-app'},
    });
    expect((event as VersionFailedEvent).error).toContain('Cache corruption detected');
    done();
  });

  mock.sendMessage({
    type: 'VERSION_FAILED',
    version: {
      hash: 'B',
      appData: {name: 'test-app'},
    },
    error: 'Cache corruption detected during resource fetch',
  });
});
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  d) Better Service Worker Debugging: Angular Now Catches Message Errors — &lt;a href="https://github.com/angular/angular/pull/62834" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When Service Workers receive corrupted or badly formatted messages, they would previously fail silently. Instead of silent failures, you now get clear logs when messages fail:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SW] Message error occurred - data could not be deserialized
[SW] Driver.onMessageError(origin: https://myapp.com)
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  e) Modern Service Workers: Angular Adds ES Module Support — &lt;a href="https://github.com/angular/angular/pull/62831" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Angular Service Workers now support ES modules with a new type configuration option, bringing modern JavaScript features like import and export to your Service Worker scripts.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Enable ES modules in Service Workers
export const appConfig: ApplicationConfig = {
  providers: [
    provideServiceWorker('ngsw-worker.js', {
      enabled: !isDevMode(),
      type: 'module', // Enable ES module features
      scope: '/app',
      updateViaCache: 'imports'
    }),
  ],
};
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  f) Service Worker Error Handling: unhandled promise rejections — &lt;a href="https://github.com/angular/angular/pull/63059" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Unhandled promise rejections happen when a Promise fails but there’s no .catch() block or error handling to deal with it:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Before: Silent failure scenario
class DataSyncService {
  syncUserPreferences() {
    // This could fail silently if the API is down
    fetch('/api/sync-preferences', {
      method: 'POST',
      body: JSON.stringify(this.preferences)
    })
    .then(response =&amp;gt; response.json())
    .then(result =&amp;gt; {
      // Update local cache
      this.updateLocalCache(result);
    });
    // No .catch() - failures would be silent!
  }
}

// After: With the new logging, you'd see in DevTools:
// "Unhandled promise rejection occurred: NetworkError: Failed to fetch"
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  5. 🔥 @angular/compiler-cli
&lt;/h3&gt;

&lt;h3&gt;
  
  
  a) Smart Template Diagnostics: Catch Function Reference Mistakes Before Runtime — &lt;a href="https://github.com/angular/angular/pull/59191" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;How many times have you written something like this and wondered why it displays [Function] instead of the actual value?&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  template: `&amp;lt;p&amp;gt;Welcome {{ getUserName }}&amp;lt;/p&amp;gt;` // Missing parentheses!
})
class WelcomeComponent {
  getUserName(): string {
    return 'Sarah';
  }
}
&lt;/code&gt;&lt;/pre&gt;



&lt;h4&gt;
  
  
  The Solution: NG8117 Diagnostic
&lt;/h4&gt;

&lt;p&gt;Angular now automatically detects this pattern and shows a clear warning:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;❌ Before:&lt;/strong&gt; Silent runtime behavior displaying [Function]&lt;br&gt;&lt;br&gt;
&lt;strong&gt;✅ Now:&lt;/strong&gt; Compile-time warning with diagnostic NG8117&lt;/p&gt;
&lt;h3&gt;
  
  
  6. 🔥 @angular/animations
&lt;/h3&gt;

&lt;p&gt;Say goodbye to @angular/animations (60KB bundle impact) and hello to native CSS animations with animate.enter and animate.leave in Angular 20.2!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.codigotipado.com/p/say-hello-to-native-css-animations" rel="noopener noreferrer"&gt;Read more here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  7. 🔥 @angular/forms
&lt;/h3&gt;
&lt;h3&gt;
  
  
  a) FormArray Gets Efficient Multi-Control Push Support — &lt;a href="https://github.com/angular/angular/pull/57102" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Previously, adding multiple controls to a FormArray required individual push() calls, each triggering change detection and validation events:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Old approach - inefficient for large datasets
const formArray = new FormArray([]);
const newControls = [
  new FormControl('value1'),
  new FormControl('value2'),
  new FormControl('value3'),
  // ... potentially hundreds more
];

// Each push triggers valueChanges, statusChanges, and validation
newControls.forEach(control =&amp;gt; {
  formArray.push(control); // Triggers events every time!
});
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The push() method now supports both individual and batch operations:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Before: Only single controls
push(control: TControl, options?: { emitEvent?: boolean }): void

// After: Single controls OR arrays of controls  
push(control: TControl | Array&amp;lt;TControl&amp;gt;, options?: { emitEvent?: boolean }): void
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  8. 🔥 @angular/router
&lt;/h3&gt;

&lt;h3&gt;
  
  
  a) Router Goes Reactive: New currentNavigation Signal Replaces Deprecated Method - &lt;a href="https://github.com/angular/angular/pull/62971" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;



&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Old approach - complex and inefficient
export class App {
  private router = inject(Router);

  // Required complex Observable setup
  isNavigating = toSignal(this.router.events.pipe(
    map(() =&amp;gt; !!this.router.getCurrentNavigation()) // Deprecated method
  ));

  // Manual state checking
  checkNavigationState() {
    const nav = this.router.getCurrentNavigation(); // Deprecated
    return nav ? 'Navigating...' : 'Idle';
  }
}

// New approach - simple and reactive
export class App {
  private router = inject(Router);

  // Clean, reactive navigation state
  isNavigating = computed(() =&amp;gt; !!this.router.currentNavigation());

  // Derive any navigation state reactively
  navigationState = computed(() =&amp;gt; {
    const nav = this.router.currentNavigation();
    return nav ? 'Navigating...' : 'Idle';
  });
}
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  9. 🔥 @angular/platform-browser
&lt;/h3&gt;

&lt;h3&gt;
  
  
  a) Warns About Hydration and Blocking Navigation Conflicts — &lt;a href="https://github.com/angular/angular/pull/62963" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When building Angular applications with server-side rendering (SSR), developers sometimes unknowingly combine features that don’t work well together:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This configuration causes subtle runtime issues
bootstrapApplication(AppComponent, {
  providers: [
    provideClientHydration(), // Enable hydration
    provideRouter(routes, 
      withEnabledBlockingInitialNavigation() // Enable blocking navigation
    )
  ]
});

// Console output:
// ⚠️ Configuration error: found both hydration and enabledBlocking initial navigation 
// in the same application, which is a contradiction.
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  b) Angular Introduces IsolatedShadowDom Encapsulation - &lt;a href="https://github.com/angular/angular/pull/62723" rel="noopener noreferrer"&gt;PR&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Ever tried building a reusable component only to have it break when used in different projects?:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Your beautiful blue button component
@Component({
  template: '&amp;lt;button class="btn"&amp;gt;Click me&amp;lt;/button&amp;gt;',
  styles: ['.btn { background: blue; color: white; }'],
  encapsulation: ViewEncapsulation.ShadowDom
})

/* Their global styles */
.btn { background: red !important; }
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Your blue button becomes red! 😱 Shadow DOM was supposed to prevent this, but Angular’s implementation leaked. The solution:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Now with TRUE isolation
@Component({
  template: '&amp;lt;button class="btn"&amp;gt;Click me&amp;lt;/button&amp;gt;',
  styles: ['.btn { background: blue; color: white; }'],
  encapsulation: ViewEncapsulation.IsolatedShadowDom // 🎉
})

// Result: Your button stays blue EVERYWHERE! 💙
&lt;/code&gt;&lt;/pre&gt;



&lt;h4&gt;
  
  
  When to Choose Each Mode
&lt;/h4&gt;

&lt;p&gt;Use ViewEncapsulation.ShadowDom when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need backwards compatibility with existing applications&lt;/li&gt;
&lt;li&gt;You want some global styles to be available in your component&lt;/li&gt;
&lt;li&gt;You’re gradually migrating to Shadow DOM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use ViewEncapsulation.IsolatedShadowDom when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building reusable component libraries&lt;/li&gt;
&lt;li&gt;Creating embeddable widgets for third-party sites&lt;/li&gt;
&lt;li&gt;Need guaranteed style isolation&lt;/li&gt;
&lt;li&gt;Following web standards precisely&lt;/li&gt;
&lt;li&gt;Building micro-frontends that must be completely isolated&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  10. 🔥 @angular/cli
&lt;/h3&gt;

&lt;p&gt;a) New &lt;a href="https://github.com/angular/angular-cli/releases/tag/20.2.0-rc.0" rel="noopener noreferrer"&gt;Angular MCP features&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Angular CLI just added powerful AI integration capabilities through MCP (Model Context Protocol) server functionality.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.codigotipado.com/p/angular-cli-202-meets-ai-the-complete" rel="noopener noreferrer"&gt;Read more here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thanks for reading so far 🙏
&lt;/h3&gt;

&lt;p&gt;I’d like to have your feedback, so please leave a &lt;strong&gt;comment&lt;/strong&gt; , &lt;strong&gt;clap&lt;/strong&gt; or &lt;strong&gt;follow&lt;/strong&gt;. &lt;em&gt;👏&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spread the Angular love! 💜&lt;/p&gt;

&lt;p&gt;If you liked it, &lt;strong&gt;share it&lt;/strong&gt; among your community, tech bros and whoever you want! 🚀👥&lt;/p&gt;

&lt;p&gt;Don’t forget to follow me and stay updated: 📱&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://www.linkedin.com/in/amos-lucian-isaila-34ab78146/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;a href="https://codigotipado.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Newsletter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@codigotipado" rel="noopener noreferrer"&gt;&lt;strong&gt;YouTube&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;a href="https://twitter.com/amosisaila" rel="noopener noreferrer"&gt;&lt;strong&gt;Twitter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for being part of this Angular journey! 👋😁&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a href="https://www.codigotipado.com/p/angular-2020-whats-new" rel="noopener noreferrer"&gt;&lt;em&gt;https://www.codigotipado.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular20</category>
      <category>mcps</category>
      <category>zoneless</category>
    </item>
    <item>
      <title>Angular CLI 20.2 Meets AI: The Complete Guide to MCP Integration</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Mon, 18 Aug 2025 05:54:16 +0000</pubDate>
      <link>https://forem.com/amosisaila/angular-cli-202-meets-ai-the-complete-guide-to-mcp-integration-4d6l</link>
      <guid>https://forem.com/amosisaila/angular-cli-202-meets-ai-the-complete-guide-to-mcp-integration-4d6l</guid>
      <description>&lt;h3&gt;
  
  
  How Angular just revolutionized AI-assisted development with Model Context Protocol.
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjsiomckcpfwsnfbpm0xc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjsiomckcpfwsnfbpm0xc.png" alt="Angular 20.2: New MCP Features" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Angular CLI 20.2 quietly introduced something that could fundamentally change how we develop Angular applications: &lt;strong&gt;Model Context Protocol (MCP) server functionality&lt;/strong&gt;. If you’re wondering what this means and why it matters, you’re in the right place.&lt;/p&gt;

&lt;p&gt;If you want to learn more about &lt;a href="https://www.codigotipado.com/p/angular-cli-2010-now-includes-ai" rel="noopener noreferrer"&gt;the beginning of the MCP servers and how Angular integrated its own, you can do it here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Angular development involves complex migrations, best practices, and architectural decisions. The Angular team recognized that AI could significantly help with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code Modernization&lt;/strong&gt;  — Automatically suggesting and applying the latest Angular patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration Guidance&lt;/strong&gt;  — Step-by-step assistance for complex framework updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practice Enforcement&lt;/strong&gt;  — Real-time suggestions for better code patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation Integration&lt;/strong&gt;  — Contextual help based on your actual code&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The New Angular MCP Features: Deep Dive
&lt;/h3&gt;

&lt;h3&gt;
  
  
  1. The modernize Tool - Your AI Migration Assistant
&lt;/h3&gt;

&lt;p&gt;The modernize tool gives AI assistants comprehensive knowledge about Angular migrations and the ability to guide you through them.&lt;/p&gt;

&lt;h4&gt;
  
  
  Available Transformations:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Control Flow Migration&lt;/li&gt;
&lt;li&gt;Signal Input/Output Migration&lt;/li&gt;
&lt;li&gt;Standalone Migration (3-Step Process) The AI assistant now understands this is a multi-step process and guides you through each phase:&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Convert Components&lt;/strong&gt; : ng g @angular/core:standalone → "Convert all components, directives and pipes to standalone"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove NgModules&lt;/strong&gt; : ng g @angular/core:standalone → "Remove unnecessary NgModule classes"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap Update&lt;/strong&gt; : ng g @angular/core:standalone → "Bootstrap the project using standalone APIs"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;How AI Uses This Information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# AI can now have conversations like this:
AI: "I notice you're using *ngIf and *ngFor in your templates. 
     Would you like me to migrate them to the new control flow syntax?"

Developer: "Yes, but just for the user components"

AI: "I'll run the control flow migration. This will update your templates 
     to use @if and @for blocks, which offer better performance and type safety."

# AI executes: ng generate @angular/core:control-flow-migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Security Controls — Three Levels of AI Access
&lt;/h3&gt;

&lt;p&gt;Angular CLI provides granular control over what AI can access and modify:&lt;/p&gt;

&lt;h4&gt;
  
  
  a) Read-Only Mode (--read-only)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng mcp --read-only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What AI Can Do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analyze your code structure&lt;/li&gt;
&lt;li&gt;Suggest improvements&lt;/li&gt;
&lt;li&gt;Provide documentation&lt;/li&gt;
&lt;li&gt;Answer questions about your codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What AI Cannot Do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modify any files&lt;/li&gt;
&lt;li&gt;Run migrations&lt;/li&gt;
&lt;li&gt;Execute commands that change your workspace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Perfect for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code reviews&lt;/li&gt;
&lt;li&gt;Architecture analysis&lt;/li&gt;
&lt;li&gt;Learning and education&lt;/li&gt;
&lt;li&gt;Untrusted AI tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  b) Local-Only Mode (--local-only)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng mcp --local-only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What This Enables:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete offline operation&lt;/li&gt;
&lt;li&gt;No external API calls&lt;/li&gt;
&lt;li&gt;Local analysis only&lt;/li&gt;
&lt;li&gt;Air-gapped development environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Corporate environments with internet restrictions&lt;/li&gt;
&lt;li&gt;Sensitive projects requiring offline development&lt;/li&gt;
&lt;li&gt;Compliance with data residency requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  c) Experimental Tools (--experimental-tool or -E)
&lt;/h4&gt;

&lt;p&gt;Think of experimental tools as “beta features” that Angular is still testing. They work, but they might change in future versions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Enable the modernize tool (currently experimental)
ng mcp --experimental-tool modernize
ng mcp -E modernize

# Enable multiple experimental tools
ng mcp -E modernize -E future-feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why This Matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safe testing of features&lt;/li&gt;
&lt;li&gt;Opt-in to capabilities&lt;/li&gt;
&lt;li&gt;Feedback collection for Angular team&lt;/li&gt;
&lt;li&gt;No surprise changes to stable workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Best Practices Integration
&lt;/h3&gt;

&lt;p&gt;The MCP server includes Angular’s comprehensive best practices guide that AI can reference in real-time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// AI can now suggest improvements like:

// Your code:
@Component({
  selector: 'app-user-list',
  template: `
    &amp;lt;div *ngFor="let user of users"&amp;gt;
      &amp;lt;app-user-card [user]="user" (click)="selectUser(user)"&amp;gt;&amp;lt;/app-user-card&amp;gt;
    &amp;lt;/div&amp;gt;
  `
})
export class UserListComponent {
  @Input() users: User[] = [];
  @Output() userSelected = new EventEmitter&amp;lt;User&amp;gt;();

  selectUser(user: User) {
    this.userSelected.emit(user);
  }
}

// AI suggestion:
"I notice several modernization opportunities:
1. Convert to standalone component
2. Use signal inputs/outputs  
3. Update to @for control flow
4. Add trackBy function for performance

Would you like me to apply these modernizations?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Real-World Usage Scenarios
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Scenario 1: Legacy Codebase Modernization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Start MCP server with modernization tools
ng mcp --experimental-tool modernize

# AI conversation:
AI: "I've analyzed your codebase. You have 47 components using old patterns. 
     I recommend this modernization path:

     1. First, convert to standalone (affects 47 files)
     2. Then migrate to signal inputs (affects 23 components)  
     3. Finally, update control flow syntax (affects 15 templates)

     Shall we start with standalone conversion?"

Developer: "Yes, but let's do it module by module"

AI: "Great! I'll start with your SharedModule. This affects 8 components 
     and should be safe to test first..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scenario 2: Code Review Assistant
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Read-only mode for safe code analysis
ng mcp --read-only

# AI can now:
AI: "I've reviewed your pull request. Here are my findings:

    ✅ Good use of standalone components
    ⚠️ Consider using signal inputs in UserComponent for better performance
    ❌ Missing trackBy in the user list - this could cause performance issues
    💡 The new @if syntax would be cleaner than *ngIf here

    Would you like specific examples for any of these suggestions?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scenario 3: Learning and Onboarding
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Safe exploration for new team members
ng mcp --read-only

# New developer conversation:
Developer: "I'm new to Angular. Can you explain this component?"

AI: "This component uses Angular's reactive forms with signal inputs. 
     Here's what each part does:

     - `userId = input.required&amp;lt;string&amp;gt;()` creates a required signal input
     - `@if (user())` uses the new control flow syntax  
     - The form uses typed reactive forms introduced in Angular 14

     This follows current Angular best practices. Would you like me to 
     explain any specific pattern in more detail?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scenario 4: Enterprise Development
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Secure, offline operation for corporate environments
ng mcp --local-only --read-only

# Benefits:
# - No external API calls
# - Complete audit trail
# - Compliance with security policies
# - Still gets intelligent code analysis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion: The AI-Native Angular Future
&lt;/h3&gt;

&lt;p&gt;Angular’s MCP integration represents a paradigm shift toward AI-native development. For the first time, AI assistants can truly understand and work with Angular projects in a secure, controlled manner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start Safe&lt;/strong&gt; : Begin with --read-only mode to explore capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual Adoption&lt;/strong&gt; : Use experimental tools for specific tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security First&lt;/strong&gt; : Leverage local-only mode for sensitive projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team Benefits&lt;/strong&gt; : Onboard new developers faster with AI assistance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Future-Ready&lt;/strong&gt; : Position your team for the AI-assisted development era&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Angular team has built this integration thoughtfully, prioritizing security and developer control. This isn’t about replacing developers — it’s about making Angular development more intelligent, efficient, and accessible.&lt;/p&gt;

&lt;p&gt;Ready to try AI-assisted Angular development? Start with ng mcp --read-only and experience the future of coding today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/cli" rel="noopener noreferrer"&gt;Angular CLI Command Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/update-guide" rel="noopener noreferrer"&gt;Angular Migration Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Thanks for reading so far 🙏
&lt;/h3&gt;

&lt;p&gt;I’d like to have your feedback, so please leave a &lt;strong&gt;comment&lt;/strong&gt; , &lt;strong&gt;clap&lt;/strong&gt; or &lt;strong&gt;follow&lt;/strong&gt;. &lt;em&gt;👏&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spread the Angular love! 💜&lt;/p&gt;

&lt;p&gt;If you liked it, &lt;strong&gt;share it&lt;/strong&gt; among your community, tech bros and whoever you want! 🚀👥&lt;/p&gt;

&lt;p&gt;Don’t forget to follow me and stay updated: 📱&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://www.linkedin.com/in/amos-lucian-isaila-34ab78146/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;a href="https://codigotipado.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Newsletter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@codigotipado" rel="noopener noreferrer"&gt;&lt;strong&gt;YouTube&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;a href="https://twitter.com/amosisaila" rel="noopener noreferrer"&gt;&lt;strong&gt;Twitter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for being part of this Angular journey! 👋😁&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a href="https://www.codigotipado.com/p/angular-cli-202-meets-ai-the-complete" rel="noopener noreferrer"&gt;&lt;em&gt;https://www.codigotipado.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular20</category>
      <category>modernize</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Say Hello to Native CSS Animations and Goodbye to @angular/animations</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Mon, 04 Aug 2025 17:00:31 +0000</pubDate>
      <link>https://forem.com/amosisaila/say-hello-to-native-css-animations-and-goodbye-to-angularanimations-i6k</link>
      <guid>https://forem.com/amosisaila/say-hello-to-native-css-animations-and-goodbye-to-angularanimations-i6k</guid>
      <description>&lt;h3&gt;
  
  
  The framework is deprecating the @angular/animations package in favor of a new approach: native CSS animations integration through animate.enter and animate.leave.
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjonsmwtwdpp991he7jw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjonsmwtwdpp991he7jw.png" alt="Angular 20.2.0: Native CSS Animations" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why @angular/animations Is Being Retired
&lt;/h3&gt;

&lt;p&gt;After eight years of service, @angular/animations is showing its age. Created before modern CSS features like @keyframes and hardware-accelerated transforms were widely supported, the package solved important problems, but now creates new ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size Impact&lt;/strong&gt; : the package adds approximately 60KB to your bundle size&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Performance Issues&lt;/strong&gt; : animations run in JavaScript without hardware acceleration&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Learning Curve&lt;/strong&gt; : Angular-specific API that doesn’t translate to other frameworks&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Integration Friction&lt;/strong&gt; : difficlt to use with popular third-party animation libraries like GSAP or Anime.js&lt;/p&gt;

&lt;p&gt;Meanwhile, the web platform has evolved dramatically, offering native animation capabilities that are faster, smaller, and more widely applicable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Meet Your New Animation System: animate.enter and animate.leave
&lt;/h3&gt;

&lt;p&gt;Starting in Angular 20.2.0, two powerful new features provide everything you need for smooth, performant animations:&lt;/p&gt;
&lt;h3&gt;
  
  
  Basic Usage: CSS Class Application
&lt;/h3&gt;

&lt;p&gt;The simplest approach applies CSS animation classes automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  template: `
    @if (showMessage()) {
      &amp;lt;div animate.enter="slide-in" animate.leave="fade-out"&amp;gt;
        Welcome to the future of Angular animations!
      &amp;lt;/div&amp;gt;
    }
    &amp;lt;button (click)="toggle()"&amp;gt;Toggle Message&amp;lt;/button&amp;gt;
  `,
  styles: [`
    .slide-in {
      animation: slideIn 0.3s ease-out;
    }

    .fade-out {
      animation: fadeOut 0.2s ease-in;
    }

    @keyframes slideIn {
      from { transform: translateY(-20px); opacity: 0; }
      to { transform: translateY(0); opacity: 1; }
    }

    @keyframes fadeOut {
      from { opacity: 1; }
      to { opacity: 0; }
    }
  `]
})
export class MessageComponent {
  showMessage = signal(false);

  toggle(): void {
    this.showMessage.update((v: boolean) =&amp;gt; !v);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Advanced Control with Animation Functions
&lt;/h3&gt;

&lt;p&gt;For complex animations or third-party library integration, use function-based control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  template: `
    &amp;lt;div class="animation-playground"&amp;gt;
      @if (showElement()) {
        &amp;lt;div class="animated-box"
             (animate.enter)="handleEnterAnimation($event)"
             (animate.leave)="handleLeaveAnimation($event)"&amp;gt;
          &amp;lt;h3&amp;gt;Advanced Animation&amp;lt;/h3&amp;gt;
          &amp;lt;p&amp;gt;Powered by GSAP integration&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      }

      &amp;lt;button (click)="toggleElement()"&amp;gt;
        {{showElement() ? 'Hide' : 'Show'}} Element
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  `,
  styles: [`
    .animated-box {
      width: 300px;
      height: 200px;
      background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
      border-radius: 12px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      color: white;
      margin: 20px 0;
    }
  `]
})
export class AdvancedAnimationComponent {
  showElement = signal(false);

  handleEnterAnimation(event: AnimationCallbackEvent): void {
    // Using GSAP for complex enter animation
    gsap.fromTo(event.target, 
      {
        scale: 0,
        rotation: -180,
        opacity: 0
      },
      {
        scale: 1,
        rotation: 0,
        opacity: 1,
        duration: 0.8,
        ease: "back.out(1.7)",
        onComplete: () =&amp;gt; {
          // Animation complete callback is automatic for enter animations
          console.log('Enter animation completed!');
        }
      }
    );
  }

  handleLeaveAnimation(event: AnimationCallbackEvent): void {
    // Complex leave animation with staggered effects
    const timeline = gsap.timeline({
      onComplete: () =&amp;gt; {
        // Must call this to complete the removal process
        event.animationComplete();
      }
    });

    timeline
      .to(event.target, {
        scale: 1.1,
        duration: 0.1,
        ease: "power2.out"
      })
      .to(event.target, {
        scale: 0,
        rotation: 180,
        opacity: 0,
        duration: 0.5,
        ease: "power2.in"
      });
  }

  toggleElement(): void {
    this.showElement.update((v: boolean) =&amp;gt; !v);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Host Element Animations
&lt;/h3&gt;

&lt;p&gt;Apply animations directly to component host elements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  selector: 'notification-card',
  template: `
    &amp;lt;div class="notification-content"&amp;gt;
      &amp;lt;h4&amp;gt;{{title()}}&amp;lt;/h4&amp;gt;
      &amp;lt;p&amp;gt;{{message()}}&amp;lt;/p&amp;gt;
      &amp;lt;button (click)="dismiss()"&amp;gt;×&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  `,
  host: {
    '[animate.enter]': 'enterAnimation',
    '[animate.leave]': 'leaveAnimation',
    'class': 'notification-card'
  },
  styles: [`
    :host {
      display: block;
      background: white;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      padding: 16px;
      margin: 8px 0;
      max-width: 400px;
    }

    .slide-in-right {
      animation: slideInRight 0.3s ease-out;
    }

    .slide-out-right {
      animation: slideOutRight 0.3s ease-in;
    }

    @keyframes slideInRight {
      from { transform: translateX(100%); opacity: 0; }
      to { transform: translateX(0); opacity: 1; }
    }

    @keyframes slideOutRight {
      from { transform: translateX(0); opacity: 1; }
      to { transform: translateX(100%); opacity: 0; }
    }
  `]
})
export class NotificationCardComponent {
  title = input.required&amp;lt;string&amp;gt;();
  message = input.required&amp;lt;string&amp;gt;();
  type = input&amp;lt;'success' | 'warning' | 'error'&amp;gt;('success');
  autoClose = input&amp;lt;boolean&amp;gt;(true);

  dismissed = output&amp;lt;void&amp;gt;();

  enterAnimation = 'slide-in-right';
  leaveAnimation = 'slide-out-right';

  // Auto-close functionality using signals
  private autoCloseTimer = signal&amp;lt;ReturnType&amp;lt;typeof setTimeout&amp;gt; | null&amp;gt;(null);

  constructor() {
    // Set up auto-close when enabled
    effect(() =&amp;gt; {
      if (this.autoClose()) {
        const timer = setTimeout(() =&amp;gt; this.dismiss(), 5000);
        this.autoCloseTimer.set(timer);
      }
    });
  }

  dismiss() {
    const timer = this.autoCloseTimer();
    if (timer) {
      clearTimeout(timer);
      this.autoCloseTimer.set(null);
    }
    this.dismissed.emit();
  }
}

// Usage in parent component with signal inputs
@Component({
  template: `
    &amp;lt;div class="notifications"&amp;gt;
      @for (notification of notifications(); track notification.id) {
        &amp;lt;notification-card 
          [title]="notification.title"
          [message]="notification.message"
          [type]="notification.type"
          [autoClose]="notification.autoClose"
          (dismissed)="removeNotification(notification.id)" /&amp;gt;
      }
    &amp;lt;/div&amp;gt;

    &amp;lt;div class="controls"&amp;gt;
      &amp;lt;button (click)="addSuccessNotification()"&amp;gt;Add Success&amp;lt;/button&amp;gt;
      &amp;lt;button (click)="addWarningNotification()"&amp;gt;Add Warning&amp;lt;/button&amp;gt;
      &amp;lt;button (click)="addErrorNotification()"&amp;gt;Add Error&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  `,
  styles: [`
    .notifications {
      position: fixed;
      top: 20px;
      right: 20px;
      z-index: 1000;
    }

    .controls {
      margin: 20px;
    }

    .controls button {
      margin-right: 10px;
      padding: 8px 16px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  `]
})
export class NotificationContainerComponent {
  notifications = signal&amp;lt;Array&amp;lt;{
    id: number;
    title: string;
    message: string;
    type: 'success' | 'warning' | 'error';
    autoClose: boolean;
  }&amp;gt;&amp;gt;([]);

  private nextId = signal(1);

  addSuccessNotification() {
    this.addNotification({
      title: 'Success!',
      message: 'Operation completed successfully!',
      type: 'success',
      autoClose: true
    });
  }

  addWarningNotification() {
    this.addNotification({
      title: 'Warning',
      message: 'Please review your settings.',
      type: 'warning',
      autoClose: false
    });
  }

  addErrorNotification() {
    this.addNotification({
      title: 'Error',
      message: 'Something went wrong. Please try again.',
      type: 'error',
      autoClose: false
    });
  }

  private addNotification(notificationData: Omit&amp;lt;any, 'id'&amp;gt;) {
    const newNotification = {
      id: this.nextId(),
      ...notificationData
    };

    this.nextId.update(id =&amp;gt; id + 1);
    this.notifications.update(notifications =&amp;gt; 
      [...notifications, newNotification]
    );
  }

  removeNotification(id: number) {
    this.notifications.update(notifications =&amp;gt; 
      notifications.filter(n =&amp;gt; n.id !== id)
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing Support
&lt;/h3&gt;

&lt;p&gt;Angular provides an ANIMATIONS_DISABLED token for test environments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TestBed.configureTestingModule({
  providers: [
    { provide: ANIMATIONS_DISABLED, useValue: true }
  ]
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When disabled, animations complete immediately while still triggering all lifecycle events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration Strategy
&lt;/h3&gt;

&lt;p&gt;For existing @angular/animations users, Angular provides a comprehensive migration guide. The most common patterns translate directly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (Angular Animations):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  animations: [
    trigger('slideIn', [
      transition(':enter', [
        style({ transform: 'translateX(-100%)' }),
        animate('300ms ease-in', style({ transform: 'translateX(0%)' }))
      ])
    ])
  ]
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (Native Animations):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Component({
  template: `&amp;lt;div animate.enter="slide-in"&amp;gt;`,
  styles: [`
    .slide-in {
      animation: slideIn 300ms ease-in;
    }
    @keyframes slideIn {
      from { transform: translateX(-100%); }
      to { transform: translateX(0%); }
    }
  `]
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Future is Bright
&lt;/h3&gt;

&lt;p&gt;This change represents more than just a new API — it’s Angular’s commitment to embracing web standards while providing the developer experience you expect. By moving to native CSS animations, Angular applications become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Faster&lt;/strong&gt; : hardware-accelerated animations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller&lt;/strong&gt; : no large animation runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More Portable&lt;/strong&gt; : skills transfer across frameworks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More Flexible&lt;/strong&gt; : easy third-party library integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The animations field in component decorators is officially deprecated as of Angular 20.2 and will be removed in version 23, giving developers a clear migration timeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F33d1jrzo5th31tyhm5f6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F33d1jrzo5th31tyhm5f6.png" alt="Benefits of Angular Native CSS Animations" width="800" height="785"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Thanks for reading so far 🙏
&lt;/h3&gt;

&lt;p&gt;I’d like to have your feedback, so please leave a &lt;strong&gt;comment&lt;/strong&gt; , &lt;strong&gt;clap&lt;/strong&gt; or &lt;strong&gt;follow&lt;/strong&gt;. &lt;em&gt;👏&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spread the Angular love! 💜&lt;/p&gt;

&lt;p&gt;If you liked it, &lt;strong&gt;share it&lt;/strong&gt; among your community, tech bros and whoever you want! 🚀👥&lt;/p&gt;

&lt;p&gt;Don’t forget to follow me and stay updated: 📱&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://www.linkedin.com/in/amos-lucian-isaila-34ab78146/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;a href="https://codigotipado.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Newsletter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@codigotipado" rel="noopener noreferrer"&gt;&lt;strong&gt;YouTube&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;a href="https://twitter.com/amosisaila" rel="noopener noreferrer"&gt;&lt;strong&gt;Twitter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for being part of this Angular journey! 👋😁&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a href="https://www.codigotipado.com/p/say-hello-to-native-css-animations" rel="noopener noreferrer"&gt;&lt;em&gt;https://www.codigotipado.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angularanimations</category>
      <category>cssnativeanimations</category>
      <category>animationsdeprecate</category>
    </item>
    <item>
      <title>Angular CLI 20.1.0 now Includes AI Integration via Model Context Protocol!</title>
      <dc:creator>Amos Isaila</dc:creator>
      <pubDate>Sun, 03 Aug 2025 23:00:43 +0000</pubDate>
      <link>https://forem.com/amosisaila/angular-cli-2010-now-includes-ai-integration-via-model-context-protocol-2bk4</link>
      <guid>https://forem.com/amosisaila/angular-cli-2010-now-includes-ai-integration-via-model-context-protocol-2bk4</guid>
      <description>&lt;h3&gt;
  
  
  The Angular CLI 20.1.0 now includes an experimental Model Context Protocol (MCP) server that lets AI assistants understand and work with your project.
&lt;/h3&gt;

&lt;h3&gt;
  
  
  The Dawn of the LLM Era
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Large Language Models&lt;/strong&gt; (LLMs) like ChatGPT, Claude, and Gemini have fundamentally changed how we think about software interaction. These AI systems, trained on vast amounts of text data, can understand and generate human-like responses. But here’s the thing: they’re incredibly powerful yet frustratingly isolated. They can write brilliant code, explain complex concepts, and solve problems, but they can’t do anything in your development environment.&lt;/p&gt;

&lt;p&gt;Imagine having a brilliant coding assistant who can tell you exactly how to fix a bug but can’t see your actual codebase, can’t run your tests, and can’t understand your project structure. That’s been the reality for most AI coding tools until now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i182bv6gcq35v6lu8jq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9i182bv6gcq35v6lu8jq.png" alt="Isolated LLM power. Born of MCP." width="721" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter the Model Context Protocol
&lt;/h3&gt;

&lt;p&gt;In November 2024, Anthropic (the company behind Claude) introduced something called the &lt;strong&gt;Model Context Protocol&lt;/strong&gt;. Think of MCP as the missing bridge between AI models and the real world of development tools. It’s an open standard that allows AI systems to securely connect to external data sources, tools, and services.&lt;/p&gt;

&lt;p&gt;Here’s what makes MCP revolutionary: instead of building custom integrations for every single tool, MCP provides a universal language that AI systems can use to communicate with any compatible service. It’s like creating a USB-C standard for AI connectivity.&lt;/p&gt;

&lt;p&gt;The protocol caught fire quickly. By March 2025, even OpenAI (Anthropic’s biggest competitor) had adopted it. Google DeepMind followed suit in April. When fierce competitors agree on a standard this quickly, you know something big is happening.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfiy0006w1mq7n5sce13.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfiy0006w1mq7n5sce13.png" alt="Universal AI Connectivity: MCP" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Angular’s Smart Move
&lt;/h3&gt;

&lt;p&gt;Now, here’s where Angular’s implementation gets interesting. The Angular team didn’t just add MCP support — they’ve positioned Angular as an AI-first development platform. When you run ng mcp, you're not just starting a server; you're making your entire Angular workspace available to AI systems in a structured, secure way.&lt;/p&gt;

&lt;p&gt;The current implementation provides two key capabilities:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Angular Best Practices as AI Knowledge:&lt;/strong&gt; The MCP server exposes Angular’s official best practices and coding guidelines directly to AI systems. This means when an AI is helping you code, it has immediate access to the latest Angular conventions, TypeScript patterns, and framework-specific guidance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Project Structure Awareness:&lt;/strong&gt; The server can list and understand your Angular workspace structure, reading from your angular.json file. This gives AI systems spatial awareness of your project – they know what apps and libraries you have, how they're organized, and can provide contextually relevant suggestions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Started
&lt;/h3&gt;

&lt;p&gt;Run ng mcp and you'll get instructions for configuring your AI host to connect to your Angular workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng mcp

// If it's the first time, it will print this:                                                                            

To start using the Angular CLI MCP Server, add this configuration to your host:

{
  "servers": {
    "angular-cli": {
      "command": "npx",
      "args": ["@angular/cli", "mcp"]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case, I’m using VS Code, and this is the configuration I need to add:  &lt;strong&gt;.vscode/mcp.json&lt;/strong&gt;. To check the configuration for other IDEs, follow the &lt;a href="https://angular.dev/ai/mcp" rel="noopener noreferrer"&gt;Angular guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, if you go to your Copilot in Agent mode (look at &lt;strong&gt;tools)&lt;/strong&gt;, you have enabled those features:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzsgqb1hedszrr4k5jea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzsgqb1hedszrr4k5jea.png" width="747" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From now on, if you chat (Copilot, Amazon Q…), these features will be applied. Remember that you can do the same in other IDEs or desktop LLM clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Angular’s MCP support might seem like a small feature update, but it represents something much bigger: the transformation of Angular from a framework you use to build applications into a platform for AI-assisted development.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thanks for reading so far 🙏
&lt;/h3&gt;

&lt;p&gt;I’d like to have your feedback so please leave a &lt;strong&gt;comment&lt;/strong&gt; , &lt;strong&gt;clap&lt;/strong&gt; or &lt;strong&gt;follow&lt;/strong&gt;. &lt;em&gt;👏&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spread the Angular love! 💜&lt;/p&gt;

&lt;p&gt;If you really liked it, &lt;strong&gt;share it&lt;/strong&gt; among your community, tech bros and whoever you want! 🚀👥&lt;/p&gt;

&lt;p&gt;Don’t forget to follow me and stay updated: 📱&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://www.linkedin.com/in/amos-lucian-isaila-34ab78146/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;a href="https://codigotipado.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Newsletter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@codigotipado" rel="noopener noreferrer"&gt;&lt;strong&gt;YouTube&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;a href="https://twitter.com/amosisaila" rel="noopener noreferrer"&gt;&lt;strong&gt;Twitter&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for being part of this Angular journey! 👋😁&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a href="https://www.codigotipado.com/p/angular-cli-2010-now-includes-ai" rel="noopener noreferrer"&gt;&lt;em&gt;https://www.codigotipado.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcpserver</category>
      <category>angular</category>
      <category>angular20</category>
      <category>mcps</category>
    </item>
  </channel>
</rss>
