<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Brian Treese</title>
    <description>The latest articles on DEV Community by Brian Treese (@brianmtreese).</description>
    <link>https://dev.to/brianmtreese</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F324503%2F0e7b14ea-502c-4967-a707-62f8a8c9a61e.jpg</url>
      <title>DEV Community: Brian Treese</title>
      <link>https://dev.to/brianmtreese</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brianmtreese"/>
    <language>en</language>
    <item>
      <title>Are We Becoming AI Managers Instead of Developers?</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Sat, 05 Sep 2026 02:08:53 +0000</pubDate>
      <link>https://dev.to/brianmtreese/are-we-becoming-ai-managers-instead-of-developers-3j52</link>
      <guid>https://dev.to/brianmtreese/are-we-becoming-ai-managers-instead-of-developers-3j52</guid>
      <description>&lt;p&gt;&lt;span&gt;I&lt;/span&gt;'ve been doing front-end development for somewhere around 20 years now. During that time, I've watched the job change over and over again. New frameworks. New languages. New tooling. New ways of building and deploying software. But I don't think I've ever had to adapt as quickly as I have over the last few years. And lately I've found myself asking a strange question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Am I becoming more of an AI manager than a software developer?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because when I look at how I work today, I barely write code anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Barely Write Code Now
&lt;/h2&gt;

&lt;p&gt;Even a year ago, I was still writing quite a bit of code myself.&lt;/p&gt;

&lt;p&gt;Today, most of my development work looks very different.&lt;/p&gt;

&lt;p&gt;I'll think through a feature, work out the requirements, ask an AI model to help me plan it, and then have an agent implement it.&lt;/p&gt;

&lt;p&gt;Sometimes I'll use one model for planning and another for implementation. Sometimes I'll break the work into smaller pieces. Sometimes I'll experiment with different approaches depending on the complexity of the problem or how much context I'm working with.&lt;/p&gt;

&lt;p&gt;Then I review the result.&lt;/p&gt;

&lt;p&gt;I make sure it works.&lt;/p&gt;

&lt;p&gt;I make sure the code makes sense.&lt;/p&gt;

&lt;p&gt;I make sure it's maintainable.&lt;/p&gt;

&lt;p&gt;And if something looks strange, I start asking questions.&lt;/p&gt;

&lt;p&gt;That's increasingly what software development looks like for me.&lt;/p&gt;

&lt;p&gt;So am I developing software?&lt;/p&gt;

&lt;p&gt;Or am I managing AI that develops software?&lt;/p&gt;

&lt;p&gt;At this point, the answer might be a little bit of both.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Thought This Would Make the Job Worse
&lt;/h2&gt;

&lt;p&gt;When AI coding tools first started getting good, I wasn't particularly excited about this version of the future.&lt;/p&gt;

&lt;p&gt;One of my first thoughts was...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Great. So now I'm just going to review code all day?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Code review has never been my favorite part of the job.&lt;/p&gt;

&lt;p&gt;The idea that AI would write everything while I sat there inspecting somebody else's work didn't sound especially appealing.&lt;/p&gt;

&lt;p&gt;But something unexpected happened as the tools improved and I got better at using them.&lt;/p&gt;

&lt;p&gt;I started enjoying it.&lt;/p&gt;

&lt;p&gt;Not necessarily because I enjoy reviewing generated code, but because there's a new problem to solve...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How effectively can I communicate what I want and get AI to produce it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's something pretty wild about describing a feature in one or two sentences and getting back something very close to what I would have built myself.&lt;/p&gt;

&lt;p&gt;Sometimes, dare I say it, I even get something better than what I would have come up with.&lt;/p&gt;

&lt;p&gt;Of course, I also get bad solutions.&lt;/p&gt;

&lt;p&gt;That's part of the job too.&lt;/p&gt;

&lt;p&gt;But when something that might have taken me several hours can suddenly be built, reviewed, adjusted, and finished in 20 minutes, it's hard not to find that exciting.&lt;/p&gt;

&lt;p&gt;The problem-solving hasn't disappeared.&lt;/p&gt;

&lt;p&gt;The problem has changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experience Feels More Valuable Right Now
&lt;/h2&gt;

&lt;p&gt;This is where things get complicated.&lt;/p&gt;

&lt;p&gt;AI can generate code extremely quickly.&lt;/p&gt;

&lt;p&gt;But being able to tell whether that code is good is still incredibly important.&lt;/p&gt;

&lt;p&gt;I've spent years working with HTML, CSS, JavaScript, Angular, PHP, WordPress, jQuery, and plenty of other technologies along the way.&lt;/p&gt;

&lt;p&gt;I've made bad architectural decisions.&lt;/p&gt;

&lt;p&gt;I've chased bugs for hours.&lt;/p&gt;

&lt;p&gt;I've implemented things one way, discovered why that approach didn't work, and rebuilt them another way.&lt;/p&gt;

&lt;p&gt;I've accumulated a huge collection of mistakes.&lt;/p&gt;

&lt;p&gt;And oddly enough, those mistakes are incredibly useful now.&lt;/p&gt;

&lt;p&gt;They help me recognize when generated code looks wrong.&lt;/p&gt;

&lt;p&gt;They help me ask better questions.&lt;/p&gt;

&lt;p&gt;They help me understand when an AI is solving the wrong problem even if the code itself looks perfectly reasonable.&lt;/p&gt;

&lt;p&gt;Right now, that experience gives experienced developers an interesting advantage.&lt;/p&gt;

&lt;p&gt;We can let AI handle more of the implementation while leaning heavily on the judgment we've developed over the years.&lt;/p&gt;

&lt;p&gt;But that raises another uncomfortable question.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do Junior Developers Get That Experience?
&lt;/h2&gt;

&lt;p&gt;This is probably the part I struggle with the most.&lt;/p&gt;

&lt;p&gt;A lot of the work we traditionally gave junior developers is exactly the type of work AI is becoming really good at.&lt;/p&gt;

&lt;p&gt;Refactor this component.&lt;/p&gt;

&lt;p&gt;Update these usages.&lt;/p&gt;

&lt;p&gt;Write this utility.&lt;/p&gt;

&lt;p&gt;Fix this bug.&lt;/p&gt;

&lt;p&gt;Add this small feature.&lt;/p&gt;

&lt;p&gt;Those tasks weren't always glamorous, but they were important.&lt;/p&gt;

&lt;p&gt;You learned by doing them.&lt;/p&gt;

&lt;p&gt;You made mistakes.&lt;/p&gt;

&lt;p&gt;You ran into weird edge cases.&lt;/p&gt;

&lt;p&gt;You discovered that the solution you thought was obvious wasn't actually very good.&lt;/p&gt;

&lt;p&gt;Over time, all of those experiences made you a better developer.&lt;/p&gt;

&lt;p&gt;So what happens when AI starts doing a large portion of that work?&lt;/p&gt;

&lt;p&gt;How does a developer who is just getting started build the judgment required to evaluate what AI produces?&lt;/p&gt;

&lt;p&gt;I don't have a great answer yet.&lt;/p&gt;

&lt;p&gt;AI can absolutely help someone learn faster than we could in the past. You can ask questions instantly, explore unfamiliar code, generate examples, compare approaches, and have complicated ideas explained in seconds.&lt;/p&gt;

&lt;p&gt;That's incredible.&lt;/p&gt;

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

&lt;p&gt;You can't assume what you're learning from AI is 100% correct.&lt;/p&gt;

&lt;p&gt;You still need enough understanding to question the answers.&lt;/p&gt;

&lt;p&gt;That's a strange place for new developers to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maybe Learning Isn't Actually That Different
&lt;/h2&gt;

&lt;p&gt;At the same time, maybe some parts of becoming a developer haven't changed as much as we think.&lt;/p&gt;

&lt;p&gt;When I was getting started, building things was basically my hobby.&lt;/p&gt;

&lt;p&gt;I built websites for family.&lt;/p&gt;

&lt;p&gt;I built websites for friends.&lt;/p&gt;

&lt;p&gt;If I heard somebody needed a website, I'd probably offer to build it.&lt;/p&gt;

&lt;p&gt;Sometimes I barely charged anything.&lt;/p&gt;

&lt;p&gt;Sometimes I didn't charge at all.&lt;/p&gt;

&lt;p&gt;I wasn't doing it because it was an efficient way to run a business.&lt;/p&gt;

&lt;p&gt;I was doing it because I wanted experience.&lt;/p&gt;

&lt;p&gt;Every project taught me something new, and eventually those projects became a portfolio that helped me get real work.&lt;/p&gt;

&lt;p&gt;If I were starting today, I think I'd probably do the same thing.&lt;/p&gt;

&lt;p&gt;The difference is that I'd have AI sitting next to me while I did it.&lt;/p&gt;

&lt;p&gt;That could actually make it possible to explore more ideas, build more projects, and learn faster.&lt;/p&gt;

&lt;p&gt;But I'd still need to stay curious about what was being generated instead of blindly accepting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  If AI Writes It, I'm Still Responsible for It
&lt;/h2&gt;

&lt;p&gt;At least for now, I have a pretty simple rule for professional software development...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If AI writes something and I don't understand what it's doing or why it's there, I'm probably leaning on AI too much.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I don't necessarily need to remember every API from memory.&lt;/p&gt;

&lt;p&gt;I don't need to manually type every line.&lt;/p&gt;

&lt;p&gt;But if I'm putting something into production, I should be able to explain what it does.&lt;/p&gt;

&lt;p&gt;I should understand the decisions being made.&lt;/p&gt;

&lt;p&gt;And I should be able to recognize when something doesn't look right.&lt;/p&gt;

&lt;p&gt;That means keeping up with the technologies I'm using is still important.&lt;/p&gt;

&lt;p&gt;When frameworks evolve, I need to know about it.&lt;/p&gt;

&lt;p&gt;When better patterns emerge, I need to understand them.&lt;/p&gt;

&lt;p&gt;And when AI produces something that feels strange, I need to question it.&lt;/p&gt;

&lt;p&gt;Sometimes that means asking another model.&lt;/p&gt;

&lt;p&gt;Sometimes it means digging into documentation.&lt;/p&gt;

&lt;p&gt;Sometimes it means throwing the implementation away and trying something different.&lt;/p&gt;

&lt;p&gt;The AI can do a lot of the typing.&lt;/p&gt;

&lt;p&gt;I still own the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are We More Productive, or Is More Just Expected of Us?
&lt;/h2&gt;

&lt;p&gt;AI definitely makes me faster.&lt;/p&gt;

&lt;p&gt;I don't think I'm suddenly a 100x developer, but I can solve certain problems dramatically faster than I could before.&lt;/p&gt;

&lt;p&gt;I've worked through some complicated problems recently that would have taken me significantly longer without AI.&lt;/p&gt;

&lt;p&gt;The frustrating part of development where you stare at a problem for hours thinking...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How am I possibly going to make this work?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;...happens less often.&lt;/p&gt;

&lt;p&gt;You can explore several possible solutions quickly.&lt;/p&gt;

&lt;p&gt;You can ask for alternatives.&lt;/p&gt;

&lt;p&gt;You can test assumptions.&lt;/p&gt;

&lt;p&gt;You can iterate much faster.&lt;/p&gt;

&lt;p&gt;That's great.&lt;/p&gt;

&lt;p&gt;But there's another side to it.&lt;/p&gt;

&lt;p&gt;When everyone knows developers can move faster, expectations move too.&lt;/p&gt;

&lt;p&gt;If a task that used to take a day can now take an hour, nobody is going to keep planning as though it takes a day forever.&lt;/p&gt;

&lt;p&gt;So yes, AI makes us more productive.&lt;/p&gt;

&lt;p&gt;But it probably also means more is expected of us.&lt;/p&gt;

&lt;p&gt;That's not necessarily unreasonable.&lt;/p&gt;

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

&lt;p&gt;The amount of work we're capable of doing changed with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When Coding Becomes Cheap?
&lt;/h2&gt;

&lt;p&gt;This might be the biggest question of all.&lt;/p&gt;

&lt;p&gt;What becomes valuable when producing code becomes incredibly cheap?&lt;/p&gt;

&lt;p&gt;For most of my career, knowing how to turn an idea into working software was a relatively scarce skill.&lt;/p&gt;

&lt;p&gt;That's changing.&lt;/p&gt;

&lt;p&gt;Today, someone with very little programming experience can already describe an idea and produce something surprisingly functional.&lt;/p&gt;

&lt;p&gt;Those tools are only getting better.&lt;/p&gt;

&lt;p&gt;So maybe the valuable part of software development increasingly becomes everything surrounding the code.&lt;/p&gt;

&lt;p&gt;Understanding the problem.&lt;/p&gt;

&lt;p&gt;Understanding the user.&lt;/p&gt;

&lt;p&gt;Making good product decisions.&lt;/p&gt;

&lt;p&gt;Knowing what should be built.&lt;/p&gt;

&lt;p&gt;Knowing what shouldn't be built.&lt;/p&gt;

&lt;p&gt;Recognizing bad solutions.&lt;/p&gt;

&lt;p&gt;Designing systems that can survive beyond the first demo.&lt;/p&gt;

&lt;p&gt;Making tradeoffs.&lt;/p&gt;

&lt;p&gt;Judgment.&lt;/p&gt;

&lt;p&gt;Right now, experience plays a massive role in all of that.&lt;/p&gt;

&lt;p&gt;But even that could change.&lt;/p&gt;

&lt;p&gt;Models are improving so quickly that it's difficult to confidently predict what any of this looks like a few years from now.&lt;/p&gt;

&lt;p&gt;Maybe eventually the implementation becomes something we barely think about at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maybe We Become Product Builders
&lt;/h2&gt;

&lt;p&gt;There's a future I can imagine where developers become less concerned with the actual mechanics of producing code.&lt;/p&gt;

&lt;p&gt;In some ways, that wouldn't be unprecedented.&lt;/p&gt;

&lt;p&gt;Think about working on a car.&lt;/p&gt;

&lt;p&gt;Changing your own oil isn't particularly difficult.&lt;/p&gt;

&lt;p&gt;Neither are plenty of other basic maintenance tasks.&lt;/p&gt;

&lt;p&gt;But most people still take their car to a mechanic.&lt;/p&gt;

&lt;p&gt;It's not because changing oil has become impossible.&lt;/p&gt;

&lt;p&gt;It's because we created an abstraction above that work.&lt;/p&gt;

&lt;p&gt;People care that the car works.&lt;/p&gt;

&lt;p&gt;They don't necessarily care about performing every step required to keep it working.&lt;/p&gt;

&lt;p&gt;Software could eventually move in a similar direction.&lt;/p&gt;

&lt;p&gt;Maybe the important skill becomes turning a business problem into a working product, while the lower-level implementation becomes increasingly automated.&lt;/p&gt;

&lt;p&gt;At that point, maybe we're not really "coders" anymore.&lt;/p&gt;

&lt;p&gt;Maybe we're product builders.&lt;/p&gt;

&lt;p&gt;And maybe that's fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would My Old Self Recognize This as Software Development?
&lt;/h2&gt;

&lt;p&gt;Absolutely not.&lt;/p&gt;

&lt;p&gt;If you showed me my current workflow ten years ago, I'm not sure I would have called it software development.&lt;/p&gt;

&lt;p&gt;I'm describing things in plain English.&lt;/p&gt;

&lt;p&gt;I'm asking agents to make changes.&lt;/p&gt;

&lt;p&gt;I'm reviewing generated implementations.&lt;/p&gt;

&lt;p&gt;I'm having conversations with software that writes software.&lt;/p&gt;

&lt;p&gt;It's weird.&lt;/p&gt;

&lt;p&gt;But I still think I'm developing.&lt;/p&gt;

&lt;p&gt;The game has changed.&lt;/p&gt;

&lt;p&gt;We're solving the same kinds of problems, but we're solving them differently.&lt;/p&gt;

&lt;p&gt;And unlike many of the changes I've seen throughout my career, this one isn't happening incrementally.&lt;/p&gt;

&lt;p&gt;It feels like the tablecloth is being pulled out from underneath us.&lt;/p&gt;

&lt;p&gt;Every month the tools get better.&lt;/p&gt;

&lt;p&gt;Every month the workflow changes a little more.&lt;/p&gt;

&lt;p&gt;We're adapting constantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do I Actually Like This Version of the Job?
&lt;/h2&gt;

&lt;p&gt;Surprisingly, yes.&lt;/p&gt;

&lt;p&gt;There have definitely been moments where I've wondered what all of this means for my career.&lt;/p&gt;

&lt;p&gt;There probably will be plenty more.&lt;/p&gt;

&lt;p&gt;But right now, this is one of the most interesting periods I've experienced as a software developer.&lt;/p&gt;

&lt;p&gt;Software development has always attracted people who like solving problems.&lt;/p&gt;

&lt;p&gt;That's still what I'm doing.&lt;/p&gt;

&lt;p&gt;I'm still thinking creatively.&lt;/p&gt;

&lt;p&gt;I'm still figuring out how things should work.&lt;/p&gt;

&lt;p&gt;I'm still building things people need.&lt;/p&gt;

&lt;p&gt;I'm just doing it with an entirely different set of tools.&lt;/p&gt;

&lt;p&gt;And I've found a new kind of enjoyment in seeing just how effectively I can use those tools.&lt;/p&gt;

&lt;p&gt;So maybe I am becoming an AI manager.&lt;/p&gt;

&lt;p&gt;Maybe we're all becoming something slightly different from what we used to call a developer.&lt;/p&gt;

&lt;p&gt;I'm not sure the terminology matters that much yet.&lt;/p&gt;

&lt;p&gt;What matters is that we're still building.&lt;/p&gt;

&lt;p&gt;The bigger question is whether &lt;strong&gt;you&lt;/strong&gt; like this version of the job.&lt;/p&gt;

&lt;p&gt;Because I think we're going to be seeing a lot more of it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>career</category>
    </item>
    <item>
      <title>AI Has Made Me Faster, But Has It Made Me Worse?</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 28 Aug 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/ai-has-made-me-faster-but-has-it-made-me-worse-2e1l</link>
      <guid>https://dev.to/brianmtreese/ai-has-made-me-faster-but-has-it-made-me-worse-2e1l</guid>
      <description>&lt;p&gt;AI has made me faster. A lot faster. But recently, I caught myself doing something that made me wonder whether that speed is coming at a cost.&lt;/p&gt;

&lt;p&gt;I’ve been a front-end developer for roughly 20 years, and these days I use AI to write most of my code.&lt;/p&gt;

&lt;p&gt;I’m still thinking through architecture, making technical decisions, reviewing what gets generated, and deciding what actually belongs in the codebase.&lt;/p&gt;

&lt;p&gt;But I’m doing far less of the actual coding than I used to.&lt;/p&gt;

&lt;p&gt;And every once in a while, I notice something that makes me wonder...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Am I becoming a better developer, or am I just getting better at using AI?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When I Realized Something Had Changed
&lt;/h2&gt;

&lt;p&gt;Recently, I was working on some animations in an Angular project.&lt;/p&gt;

&lt;p&gt;I was using AI to help with the implementation, and it generated some CSS using a property that I knew was relatively new.&lt;/p&gt;

&lt;p&gt;My immediate reaction was...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Is this actually supported by all the browsers we need to support?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few years ago, I probably would have known the answer already. &lt;/p&gt;

&lt;p&gt;I probably would have opened caniuse.com, checked the support table, and moved on.&lt;/p&gt;

&lt;p&gt;But I would've also been the one intentionally reaching for it to build out the solution myself.&lt;/p&gt;

&lt;p&gt;This time, I just asked the AI.&lt;/p&gt;

&lt;p&gt;It told me the browser support was good, gave me some statistics, and I accepted the answer.&lt;/p&gt;

&lt;p&gt;That was the moment where I stopped and thought...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Wait… am I getting worse at this?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not because the AI gave me a bad answer. It didn’t.&lt;/p&gt;

&lt;p&gt;The uncomfortable part was that I didn’t really feel the need to verify it myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  I’m Learning Different Things Now
&lt;/h2&gt;

&lt;p&gt;Web development has always required us to keep learning.&lt;/p&gt;

&lt;p&gt;When I started, my focus was heavily on HTML and CSS. Then JavaScript became a bigger part of my work. Then frameworks like Angular became central to what I was doing.&lt;/p&gt;

&lt;p&gt;There was always something new to understand.&lt;/p&gt;

&lt;p&gt;That part hasn’t changed.&lt;/p&gt;

&lt;p&gt;What has changed is where a lot of my attention goes.&lt;/p&gt;

&lt;p&gt;I still care about browser APIs, Angular features, CSS, performance, accessibility, and all the normal things that come with front-end development.&lt;/p&gt;

&lt;p&gt;But now I’m also spending a lot of time trying to understand AI tools.&lt;/p&gt;

&lt;p&gt;Which models are better for which tasks?&lt;/p&gt;

&lt;p&gt;How should I structure a prompt?&lt;/p&gt;

&lt;p&gt;How much context should I give it?&lt;/p&gt;

&lt;p&gt;Should I let it make a larger change or break the problem into smaller pieces?&lt;/p&gt;

&lt;p&gt;When should I trust it, and when should I push back?&lt;/p&gt;

&lt;p&gt;Those are development skills now too, whether we like it or not.&lt;/p&gt;

&lt;p&gt;And because AI is changing so quickly, there’s a lot to keep up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Am I Actually Getting Worse?
&lt;/h2&gt;

&lt;p&gt;I think this is where the question gets more complicated.&lt;/p&gt;

&lt;p&gt;If you asked me whether I’m becoming better at manually writing code, the answer is probably no.&lt;/p&gt;

&lt;p&gt;I’m doing less of it.&lt;/p&gt;

&lt;p&gt;So I’m sure there are things I could once recall immediately that I now might have to look up (or ask AI).&lt;/p&gt;

&lt;p&gt;But I don’t think that automatically means I’m becoming worse as a developer.&lt;/p&gt;

&lt;p&gt;Because at the same time, I’m getting better at other parts of development.&lt;/p&gt;

&lt;p&gt;I’m spending more time thinking about the shape of the solution before any code gets written.&lt;/p&gt;

&lt;p&gt;I’m reviewing implementations more critically.&lt;/p&gt;

&lt;p&gt;I’m deciding what should change, what shouldn’t change, and whether the code AI generated actually solves the problem in the right way.&lt;/p&gt;

&lt;p&gt;And sometimes AI shows me something I genuinely wouldn’t have thought of myself.&lt;/p&gt;

&lt;p&gt;It might find an edge case I missed.&lt;/p&gt;

&lt;p&gt;It might use an approach that is cleaner than the one I had in mind.&lt;/p&gt;

&lt;p&gt;It might introduce me to an API or technique I haven’t used before.&lt;/p&gt;

&lt;p&gt;When that happens, I’m still learning.&lt;/p&gt;

&lt;p&gt;Just in a different way.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI as a Pair Programmer
&lt;/h2&gt;

&lt;p&gt;That’s probably the best way I can describe how I use AI today.&lt;/p&gt;

&lt;p&gt;It feels a lot like pair programming.&lt;/p&gt;

&lt;p&gt;Except instead of sitting next to another developer and taking turns writing code, I’m describing the problem, reviewing the implementation, correcting mistakes, and refining the solution.&lt;/p&gt;

&lt;p&gt;Sometimes the AI gets it wrong.&lt;/p&gt;

&lt;p&gt;Sometimes it misunderstands what I’m asking.&lt;/p&gt;

&lt;p&gt;Sometimes it generates something way more complicated than it needs to be.&lt;/p&gt;

&lt;p&gt;And I have to recognize that and fix it.&lt;/p&gt;

&lt;p&gt;Other times, it produces something better than what I would have written myself.&lt;/p&gt;

&lt;p&gt;That doesn’t make me feel like I’ve lost control.&lt;/p&gt;

&lt;p&gt;I still decide what gets used.&lt;/p&gt;

&lt;p&gt;I still decide how much of the problem I want AI to handle.&lt;/p&gt;

&lt;p&gt;Sometimes I ask it for a tiny change.&lt;/p&gt;

&lt;p&gt;Sometimes I let it build out a larger feature.&lt;/p&gt;

&lt;p&gt;Sometimes I reject the whole thing and start over.&lt;/p&gt;

&lt;p&gt;The tool is powerful, but I’m still responsible for the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is Probably Very Different for Junior Developers
&lt;/h2&gt;

&lt;p&gt;I also think experience changes this conversation quite a bit.&lt;/p&gt;

&lt;p&gt;I’ve spent two decades writing code, debugging bad solutions, making architectural mistakes, refactoring things I thought were good ideas at the time, and learning what tends to go wrong.&lt;/p&gt;

&lt;p&gt;That experience gives me a pretty good filter for evaluating what AI produces.&lt;/p&gt;

&lt;p&gt;Someone at the beginning of their career doesn’t have that yet.&lt;/p&gt;

&lt;p&gt;And I think that’s what makes this moment especially difficult for junior developers.&lt;/p&gt;

&lt;p&gt;They need to build that experience.&lt;/p&gt;

&lt;p&gt;They need to struggle through problems.&lt;/p&gt;

&lt;p&gt;They need to write things that don’t work and understand why they don’t work.&lt;/p&gt;

&lt;p&gt;They need to learn what good code looks like.&lt;/p&gt;

&lt;p&gt;But at the same time, they’re entering an industry where they’re probably going to be expected to use AI.&lt;/p&gt;

&lt;p&gt;The bigger challenge is figuring out how junior developers can use AI without skipping the learning that eventually gives them good judgment.&lt;/p&gt;

&lt;p&gt;I’m not sure we’ve figured that out yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maybe the Job Is Just Changing
&lt;/h2&gt;

&lt;p&gt;For a long time, being a good developer meant knowing how to translate a solution into code.&lt;/p&gt;

&lt;p&gt;That still matters today.&lt;/p&gt;

&lt;p&gt;But I’m not convinced it will matter in exactly the same way forever.&lt;/p&gt;

&lt;p&gt;If AI keeps improving, I wouldn’t be surprised if we spend less time thinking about the mechanics of writing code and more time thinking about the system around it.&lt;/p&gt;

&lt;p&gt;What should we build?&lt;/p&gt;

&lt;p&gt;How should it behave?&lt;/p&gt;

&lt;p&gt;How should the pieces fit together?&lt;/p&gt;

&lt;p&gt;What are the tradeoffs?&lt;/p&gt;

&lt;p&gt;What could go wrong?&lt;/p&gt;

&lt;p&gt;Is the implementation actually correct?&lt;/p&gt;

&lt;p&gt;Is it maintainable?&lt;/p&gt;

&lt;p&gt;Does it solve the right problem?&lt;/p&gt;

&lt;p&gt;Those questions aren’t going away just because AI can generate code.&lt;/p&gt;

&lt;p&gt;If anything, they may become more important.&lt;/p&gt;

&lt;p&gt;So yes, in some ways I may be losing some manual coding sharpness.&lt;/p&gt;

&lt;p&gt;But I’m also gaining new skills, learning from AI-generated solutions, and spending more time on parts of development that I think are becoming increasingly important.&lt;/p&gt;

&lt;p&gt;That doesn’t feel like becoming worse as a developer.&lt;/p&gt;

&lt;p&gt;It feels like becoming a different kind of developer.&lt;/p&gt;

&lt;p&gt;And maybe that’s the real question.&lt;/p&gt;

&lt;p&gt;Are we actually getting worse at development, or are the skills that define a good developer simply changing?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What is 20 years of coding experience worth now?</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Mon, 24 Aug 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/what-is-20-years-of-coding-experience-worth-now-1g9o</link>
      <guid>https://dev.to/brianmtreese/what-is-20-years-of-coding-experience-worth-now-1g9o</guid>
      <description>&lt;p&gt;I’ve been a front-end developer for more than 20 years now. For the last decade or so, most of my work has centered around Angular. I’ve spent a huge part of my career learning how to write better code, debug faster, understand architecture, recognize patterns, and generally become a more capable developer.&lt;/p&gt;

&lt;p&gt;For a long time, that experience felt like a pretty clear advantage.&lt;/p&gt;

&lt;p&gt;Now I’m not so sure what that advantage is supposed to look like.&lt;/p&gt;

&lt;p&gt;AI has changed the way I work enough that I sometimes find myself wondering what 20 years of coding experience is actually worth when so much of the coding itself can now be done for me.&lt;/p&gt;

&lt;p&gt;And depending on the day, I can feel really excited about that or pretty uneasy about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Barely Write Code the Way I Used To
&lt;/h2&gt;

&lt;p&gt;One of the strangest changes for me is that I rarely spend much time manually writing code anymore.&lt;/p&gt;

&lt;p&gt;Most of the time, I’m essentially coding in plain English.&lt;/p&gt;

&lt;p&gt;I’ll explain what I want to accomplish, let AI take a pass at it, review what it produced, and then continue refining it through conversation.&lt;/p&gt;

&lt;p&gt;A few years ago, I probably would’ve hated that idea.&lt;/p&gt;

&lt;p&gt;I used to enjoy being the person who could sit down and build something by hand. There was a certain satisfaction in knowing the APIs, understanding the framework, and being able to turn an idea into working code myself.&lt;/p&gt;

&lt;p&gt;If I’m being completely transparent, I probably took some pride in feeling like I was better at that than a lot of other developers.&lt;/p&gt;

&lt;p&gt;AI has changed that feeling quite a bit.&lt;/p&gt;

&lt;p&gt;The interesting part, though, is that I don’t feel like my experience has become useless.&lt;/p&gt;

&lt;p&gt;I feel like I’m just using it differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  So What Are 20 Years of Experience Worth Now?
&lt;/h2&gt;

&lt;p&gt;The AI can write the code.&lt;/p&gt;

&lt;p&gt;What it can’t consistently do, at least in my experience, is know whether the code it wrote is actually the right code.&lt;/p&gt;

&lt;p&gt;That’s where the experience still matters.&lt;/p&gt;

&lt;p&gt;When AI gives me an implementation, I’m constantly evaluating it.&lt;/p&gt;

&lt;p&gt;Does this fit the architecture?&lt;/p&gt;

&lt;p&gt;Is this solving the actual problem?&lt;/p&gt;

&lt;p&gt;Did it introduce unnecessary complexity?&lt;/p&gt;

&lt;p&gt;Is there already something in the codebase we should be using?&lt;/p&gt;

&lt;p&gt;What else does this change affect?&lt;/p&gt;

&lt;p&gt;Is this going to create a problem six months from now?&lt;/p&gt;

&lt;p&gt;Those questions aren’t really about syntax.&lt;/p&gt;

&lt;p&gt;They’re about judgment.&lt;/p&gt;

&lt;p&gt;And a lot of that judgment comes from years of building things, breaking things, maintaining things, refactoring bad decisions, and learning what tends to work in real applications.&lt;/p&gt;

&lt;p&gt;In that sense, AI hasn’t made my experience irrelevant.&lt;/p&gt;

&lt;p&gt;It may actually be forcing me to rely on the parts of that experience that matter most.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Works Best for Me in Small Chunks
&lt;/h2&gt;

&lt;p&gt;I’ve also found that the way I use AI matters a lot.&lt;/p&gt;

&lt;p&gt;I see plenty of demos where someone gives an agent a large feature, walks away, and comes back to a finished implementation.&lt;/p&gt;

&lt;p&gt;That hasn’t really been my experience.&lt;/p&gt;

&lt;p&gt;What works best for me is keeping the tasks small.&lt;/p&gt;

&lt;p&gt;I’ll give AI one focused piece of work, review it, make sure it’s right, and then move on to the next piece.&lt;/p&gt;

&lt;p&gt;In a lot of ways, it’s not that different from how I would have built the feature myself.&lt;/p&gt;

&lt;p&gt;I’m still thinking through the structure.&lt;/p&gt;

&lt;p&gt;I’m still deciding what should happen next.&lt;/p&gt;

&lt;p&gt;I’m still controlling the direction.&lt;/p&gt;

&lt;p&gt;The difference is that AI is doing much more of the implementation work along the way.&lt;/p&gt;

&lt;p&gt;That workflow has been surprisingly effective for me.&lt;/p&gt;

&lt;p&gt;It’s faster, but more importantly, it can make me more thorough.&lt;/p&gt;

&lt;p&gt;Sometimes I’ll ask AI to refactor something and it’ll notice another related area that needs to change that I might not have remembered immediately.&lt;/p&gt;

&lt;p&gt;That part is genuinely useful.&lt;/p&gt;

&lt;p&gt;I’m not suddenly 100 times more productive, but I can definitely get more done.&lt;/p&gt;

&lt;p&gt;And in some ways, I actually enjoy the work more now.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Feels a Lot Like a Very Fast Junior Developer
&lt;/h2&gt;

&lt;p&gt;The closest comparison I can make is that working with AI sometimes feels like working with an incredibly fast junior developer.&lt;/p&gt;

&lt;p&gt;You explain what you need.&lt;/p&gt;

&lt;p&gt;It goes off and does it almost immediately.&lt;/p&gt;

&lt;p&gt;Then you look at what it came back with and say:&lt;/p&gt;

&lt;p&gt;No, not quite.&lt;/p&gt;

&lt;p&gt;Let’s do this differently.&lt;/p&gt;

&lt;p&gt;Don’t add that.&lt;/p&gt;

&lt;p&gt;Use this pattern instead.&lt;/p&gt;

&lt;p&gt;You send it back, and a few seconds later it returns with another version.&lt;/p&gt;

&lt;p&gt;That’s incredibly powerful.&lt;/p&gt;

&lt;p&gt;But it also explains why I still don’t feel comfortable handing over large parts of a production application and simply trusting the result.&lt;/p&gt;

&lt;p&gt;The speed is there.&lt;/p&gt;

&lt;p&gt;The judgment isn’t always there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Big Agent Workflows Still Haven’t Clicked for Me
&lt;/h2&gt;

&lt;p&gt;This is one area where I still feel like I might be missing something.&lt;/p&gt;

&lt;p&gt;I see developers talking about multi-agent environments, autonomous coding workflows, and handing entire features over to AI.&lt;/p&gt;

&lt;p&gt;I’ve tried versions of that.&lt;/p&gt;

&lt;p&gt;Usually, I end up with a massive code review.&lt;/p&gt;

&lt;p&gt;The AI touches more things than I expected. It makes architectural decisions I wouldn’t have made. It introduces abstractions I don’t necessarily want.&lt;/p&gt;

&lt;p&gt;At that point, figuring out whether everything is correct can take longer than I expected.&lt;/p&gt;

&lt;p&gt;So for now, I keep coming back to smaller, controlled iterations.&lt;/p&gt;

&lt;p&gt;Maybe that changes.&lt;/p&gt;

&lt;p&gt;Maybe other teams have already figured out much better agent workflows than I have.&lt;/p&gt;

&lt;p&gt;If that’s you, I’d genuinely like to know what you’re doing differently.&lt;/p&gt;

&lt;p&gt;What tools are you using?&lt;/p&gt;

&lt;p&gt;How much context are you giving the agents?&lt;/p&gt;

&lt;p&gt;How much review is still required afterward?&lt;/p&gt;

&lt;p&gt;And are you really trusting these systems with large production features?&lt;/p&gt;

&lt;p&gt;I’m interested in the workflows people are actually using every day, not just the ones that make for an impressive demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Experienced Developers Still Matter?
&lt;/h2&gt;

&lt;p&gt;Right now, I think we do.&lt;/p&gt;

&lt;p&gt;But I also think the reason we matter is changing.&lt;/p&gt;

&lt;p&gt;Being able to write code quickly may become less valuable.&lt;/p&gt;

&lt;p&gt;Knowing what should be built, how it should fit together, whether the implementation is good, and what risks are hiding inside it may become much more important.&lt;/p&gt;

&lt;p&gt;Maybe the job shifts away from writing every line yourself and more toward directing, evaluating, and shaping what gets built.&lt;/p&gt;

&lt;p&gt;I don’t know exactly where this ends.&lt;/p&gt;

&lt;p&gt;And I don’t want to pretend that I do.&lt;/p&gt;

&lt;p&gt;There are days when AI makes me incredibly excited about what I’m able to accomplish.&lt;/p&gt;

&lt;p&gt;There are other days when I look at how fast all of this is moving and wonder what it means for the career I’ve spent more than two decades building.&lt;/p&gt;

&lt;p&gt;For now, though, I still feel needed.&lt;/p&gt;

&lt;p&gt;I still feel like the experience I’ve accumulated matters.&lt;/p&gt;

&lt;p&gt;I’m just using it in a very different way than I expected.&lt;/p&gt;

&lt;p&gt;That’s how I feel today, anyway.&lt;/p&gt;

&lt;p&gt;Ask me again in a few weeks. 😂&lt;/p&gt;

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

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

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

&lt;p&gt;In this demo, we have a simple invite link with a button beside it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-invite-link.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-invite-link.jpg" alt="Example of a simple invite link with a button beside it" width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;p&gt;This directive owns the tooltip message, host attributes, and temporary message state:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Directive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[appTooltip]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;has-tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[attr.aria-label]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibleMessage()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[attr.data-tooltip]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibleMessage()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[class.show-tooltip]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showingTemporaryMessage()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TooltipDirective&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;destroyRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DestroyRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;temporaryMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destroyRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDestroy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;showingTemporaryMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;temporaryMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;visibleMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;temporaryMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temporaryMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;clearMessage&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temporaryMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;showTemporaryMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;temporaryMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few important details here.&lt;/p&gt;

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

&lt;p&gt;It also adds several host bindings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;has-tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[attr.aria-label]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibleMessage()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[attr.data-tooltip]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibleMessage()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[class.show-tooltip]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showingTemporaryMessage()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;aria-label&lt;/code&gt; gives the button an accessible name.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Next, we have a directive for the copy behavior:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Directive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[appCopyButton]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hostDirectives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;directive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message: tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(click)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CopyButtonDirective&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;copyText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;copiedMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;copyFailedMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;copiedDuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;tooltip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyText&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showTemporaryMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copiedMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copiedDuration&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showTemporaryMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyFailedMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copiedDuration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This directive composes the &lt;code&gt;TooltipDirective&lt;/code&gt; using &lt;code&gt;hostDirectives&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;hostDirectives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;directive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message: tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means when &lt;code&gt;appCopyButton&lt;/code&gt; is applied to an element, Angular also applies the tooltip behavior to that same host element.&lt;/p&gt;

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

&lt;p&gt;So instead of writing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;appTooltip&lt;/span&gt; &lt;span class="na"&gt;message=&lt;/span&gt;&lt;span class="s"&gt;"Copy invite link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can expose a more purposeful API through the copy directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;appCopyButton&lt;/span&gt; &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"Copy invite link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

&lt;p&gt;This one handles the pressed state:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Directive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[appPressable]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hostDirectives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;directive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message: tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(pointerdown)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;press()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(pointerup)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;release()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(pointerleave)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;release()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(blur)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;release()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[class.is-pressed]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;isPressed()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PressableDirective&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;pressedMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;isPressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;tooltip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TooltipDirective&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;press&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pressedMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPressed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tooltip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This directive also composes the same &lt;code&gt;TooltipDirective&lt;/code&gt;.&lt;/p&gt;

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;p&gt;In the app component, we import both behavior directives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CopyButtonDirective&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./directives/copy-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PressableDirective&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./directives/pressable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CopyButtonDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PressableDirective&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nx"&gt;inviteUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/articles/angular-host-directives&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the template, we layer both behaviors onto the same button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;appPressable&lt;/span&gt;
  &lt;span class="na"&gt;pressedMessage=&lt;/span&gt;&lt;span class="s"&gt;"Release to copy"&lt;/span&gt;
  &lt;span class="na"&gt;appCopyButton&lt;/span&gt;
  &lt;span class="na"&gt;tooltip=&lt;/span&gt;&lt;span class="s"&gt;"Copy invite link"&lt;/span&gt;
  &lt;span class="na"&gt;copiedMessage=&lt;/span&gt;&lt;span class="s"&gt;"Copied!"&lt;/span&gt;
  &lt;span class="na"&gt;copyFailedMessage=&lt;/span&gt;&lt;span class="s"&gt;"Unable to copy"&lt;/span&gt;
  &lt;span class="na"&gt;[copyText]=&lt;/span&gt;&lt;span class="s"&gt;"inviteUrl()"&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"icon-button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;🔗&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still just a button.&lt;/p&gt;

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

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

&lt;p&gt;This is the kind of composition that should be possible with host directives.&lt;/p&gt;




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

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

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

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

&lt;p&gt;Before Angular 22, this setup would throw a duplicate directive error:&lt;/p&gt;


&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-duplicate-directive-error.jpg" alt="Example of a duplicate directive error" width="800" height="392"&gt;


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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;p&gt;Now the button works as expected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-button-works-as-expected.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F07-02%2Fangular-host-directives-deduplicate-shared-directives-button-works-as-expected.png" alt="Example of a button working as expected" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Both directives expose the tooltip message input using the same alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message: tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That matters.&lt;/p&gt;

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

&lt;p&gt;So this is fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message: tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But you can't do this in the other directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message: other-tooltip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one directive exposed the same input as &lt;code&gt;tooltip&lt;/code&gt; and another exposed it as something else, Angular would not know which public API should win.&lt;/p&gt;

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

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

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

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

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

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

&lt;p&gt;That makes directive composition feel a lot more practical.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-host-directives-deduplicate-shared-directives" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/directives/directive-composition-api?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Directive Composition API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/9c55fcb3e65ffcde32d7ac438ea40a69ffc2b3b6" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/errors/NG8024?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;NG8024: Conflicting Host Directive Binding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;My course "Angular Signal Forms: Build Modern Forms with Signals"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Angular 22: Better Tab Animations in Material</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/better-angular-material-tab-animations-in-angular-22-3930</link>
      <guid>https://dev.to/brianmtreese/better-angular-material-tab-animations-in-angular-22-3930</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular &lt;a href="https://material.angular.dev/components/tabs/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Material tabs&lt;/a&gt; already make it easy to organize content into separate views, but the animation controls have always had one slightly annoying limitation: the tab indicator and the tab body shared the same animation duration. In Angular 22, that changes. Now, &lt;code&gt;mat-tab-group&lt;/code&gt; can use separate values for the tab header and tab body animation durations, giving us more fine-grained control over the animation.&lt;/p&gt;

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

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

&lt;p&gt;Let's say we have a simple client workspace dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-25%2Fangular-material-tabs-client-dashboard.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-25%2Fangular-material-tabs-client-dashboard.jpg" alt="Example of a simple client workspace dashboard built using Angular Material tabs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This app uses &lt;a href="https://material.angular.dev/components/tabs/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Material tabs&lt;/a&gt; to separate the client overview, projects, invoices, and recent activity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mat-tab-group&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mat-tab&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Overview"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="nt"&gt;&amp;lt;/mat-tab&amp;gt;&lt;/span&gt;

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;ol&gt;
&lt;li&gt;The active tab indicator moves in the tab header&lt;/li&gt;
&lt;li&gt;The tab content slides in the tab body&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-25%2Fangular-material-tabs-animation-duration.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-25%2Fangular-material-tabs-animation-duration.jpg" alt="Example of the two different animations that happen when switching between tabs in Angular Material tabs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

&lt;p&gt;So, if we wanted to slow down the tab transition, we could do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mat-tab-group&lt;/span&gt; &lt;span class="na"&gt;animationDuration=&lt;/span&gt;&lt;span class="s"&gt;"1500ms"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;p&gt;We can try to fix the sluggish tab indicator by lowering the duration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mat-tab-group&lt;/span&gt; &lt;span class="na"&gt;animationDuration=&lt;/span&gt;&lt;span class="s"&gt;"150ms"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;p&gt;And now in Angular 22, they can.&lt;/p&gt;




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

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

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

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

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

&lt;p&gt;So instead of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mat-tab-group&lt;/span&gt; &lt;span class="na"&gt;animationDuration=&lt;/span&gt;&lt;span class="s"&gt;"150ms"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can bind to an object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mat-tab-group&lt;/span&gt; &lt;span class="na"&gt;[animationDuration]=&lt;/span&gt;&lt;span class="s"&gt;"{
  header: '150ms',
  body: '1500ms'
}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/mat-tab-group&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;It's not a huge feature, but it removes a real UI compromise.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-material-tabs-separate-animation-durations" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/components/commit/85c16fe4bb68d89d1f304350ecf965aeceea4662" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/components/tabs/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Material Tabs Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/?couponCode=021409EC66FC6440B867" rel="noopener noreferrer"&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

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

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

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

&lt;p&gt;Let's start with a simple profile settings screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-18%2Fangular-linked-signal-custom-setter-profile-settings-screen.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-18%2Fangular-linked-signal-custom-setter-profile-settings-screen.png" alt="Example of a simple profile settings screen in Angular" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;In the template, we bind the input to a field-level signal &lt;code&gt;maxMarketingEmailsPerWeek()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
  &lt;span class="na"&gt;#emailLimit&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt;
  &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;[value]=&lt;/span&gt;&lt;span class="s"&gt;"maxMarketingEmailsPerWeek()"&lt;/span&gt;
  &lt;span class="na"&gt;(input)=&lt;/span&gt;&lt;span class="s"&gt;"setMaxMarketingEmailsPerWeek(emailLimit.value)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we have a couple of buttons that update that value from their own methods &lt;code&gt;sendFewerEmails()&lt;/code&gt; and &lt;code&gt;sendMoreEmails()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;[disabled]=&lt;/span&gt;&lt;span class="s"&gt;"maxMarketingEmailsPerWeek() === 0"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"sendFewerEmails()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Send fewer emails
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"sendMoreEmails()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Send more emails
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;The actual source of truth is a larger profile object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Brian Treese&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brian@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A profile object like this might come from an API, a resource, a store, a parent component, or some other state layer.&lt;/p&gt;

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

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

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

&lt;p&gt;Before this update, we could use &lt;code&gt;linkedSignal()&lt;/code&gt; to read the field from the parent signal:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

&lt;p&gt;First, the input handler received the raw DOM value as a string, converted it to a number, prevented negative values, and then called a &lt;code&gt;updateMaxMarketingEmailsPerWeek()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;setMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then that method updated the parent &lt;code&gt;profile&lt;/code&gt; signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The buttons needed to call the same method too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendFewerEmails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendMoreEmails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

&lt;p&gt;That's the part Angular 22 lets us clean up.&lt;/p&gt;




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

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

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

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

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

&lt;p&gt;That means we can keep the read logic and write-back logic together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now this linked signal knows how to read from the profile (which it already did):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it also knows how to write back to the profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

&lt;p&gt;Instead of calling the separate helper method, we can call &lt;code&gt;set()&lt;/code&gt; directly on the linked signal.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;setMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;setMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function still handles the DOM input conversion, but once we have the final value, we just set the field-level signal.&lt;/p&gt;

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

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

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

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

&lt;p&gt;Instead of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendFewerEmails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendMoreEmails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can update the linked signal directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendFewerEmails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;p&gt;We have the source of truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Brian Treese&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brian@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we have the field-level linked signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linkedSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the UI actions can work directly with that field-level signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;setMaxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;sendFewerEmails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxMarketingEmailsPerWeek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



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

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

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

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

&lt;p&gt;Now the linked signal can own both sides.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-linkedsignal-custom-setter" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/124ba10ead58c9f93b0b74c4102022c4674db1f5" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/linkedSignal?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular linkedSignal() API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/signals?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Signals Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/angular-signal-forms/" rel="noopener noreferrer"&gt;Angular Signal Forms Course&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

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

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

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

&lt;p&gt;In this example, we have a simple checkout screen with a “Need help?” area in the sidebar:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel.png" alt="A screenshot of the checkout screen with a sidebar and a help area" width="799" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the user clicks the button, we render a payment help message: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel-rendered.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel-rendered.png" alt="A screenshot of the checkout screen after the Need help button is clicked, showing a payment help message rendered in the sidebar" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

&lt;p&gt;Then, we render it again as a floating support panel attached directly to the document body with &lt;code&gt;DomPortalOutlet&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel-floating.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-11%2Fangular-cdk-component-portal-checkout-help-panel-floating.png" alt="A screenshot of the checkout screen with the payment help message rendered as a floating support panel over the page" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;The inline version uses &lt;code&gt;CdkPortalOutlet&lt;/code&gt; in the checkout template.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ng-template&lt;/span&gt; &lt;span class="na"&gt;[cdkPortalOutlet]=&lt;/span&gt;&lt;span class="s"&gt;"helpPortal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ng-template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the component, we create the portal only when the help panel should be shown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;showHelp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;helpPortal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

&lt;p&gt;Instead of rendering into a template outlet, we create a DOM element manually and attach a portal there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;openSupport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;floating-help&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outlet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DomPortalOutlet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;injector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outlet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supportOpen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when the content should not be constrained by the current component layout.&lt;/p&gt;

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

&lt;p&gt;So now we have two rendering strategies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Inline checkout help&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



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

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

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

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

&lt;p&gt;Instead of adding this behavior to the help component itself, we can create a directive.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Directive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[appPortalPanel]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;region&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[attr.aria-label]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tabindex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PortalPanelDirective&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;ariaLabel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;elementRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ElementRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;afterNextRender&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This directive does three things.&lt;/p&gt;

&lt;p&gt;First, it adds the &lt;code&gt;region&lt;/code&gt; role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;region&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it binds the &lt;code&gt;aria-label&lt;/code&gt; from an input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[attr.aria-label]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel()&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, it adds &lt;code&gt;tabindex="-1"&lt;/code&gt; so the panel can receive programmatic focus without adding it to the normal tab order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tabindex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The focus behavior happens after Angular finishes rendering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;afterNextRender&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nativeElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the behavior we want, but the important part is where we apply it.&lt;/p&gt;

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

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

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

&lt;p&gt;That’s all contextual behavior, so we’ll attach it from the portal instead.&lt;/p&gt;




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

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

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

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

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

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

&lt;p&gt;Here's what it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;helpPortal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PortalPanelDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment help for checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;So we pass through the earlier optional arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// viewContainerRef&lt;/span&gt;
  &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// injector&lt;/span&gt;
  &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// projectableNodes&lt;/span&gt;
  &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// bindings&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// Directives&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key part is the &lt;code&gt;directives&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PortalPanelDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment help for checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; tells Angular which directive to apply.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Now we can apply the same idea to the floating support launcher.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outlet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ComponentPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;PaymentHelpComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PortalPanelDirective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Floating payment help&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same component.&lt;/p&gt;

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

&lt;p&gt;But now the label is different because the rendering context is different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ariaLabel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Floating payment help&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

&lt;p&gt;And when we inspect the generated host element, it now includes the attributes we need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;app-payment-help&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"region"&lt;/span&gt;
  &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Payment help for checkout"&lt;/span&gt;
  &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/app-payment-help&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The floating panel gets the same behavior too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;app-payment-help&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"region"&lt;/span&gt;
  &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Floating payment help"&lt;/span&gt;
  &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/app-payment-help&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;p&gt;And the portal becomes the place where dynamic rendering and context-specific behavior come together.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-cdk-component-portal-directives" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/cdk/portal/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular CDK Portal Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/inputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular inputBinding API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Angular Material Dialogs Now Support Real Component Bindings</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 05 Jun 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-material-dialogs-now-support-real-component-bindings-2pmc</link>
      <guid>https://dev.to/brianmtreese/angular-material-dialogs-now-support-real-component-bindings-2pmc</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular &lt;a href="https://material.angular.dev/components/dialog/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Material dialogs&lt;/a&gt; just got a practical upgrade. In Angular 22, dialogs can now bind directly to component inputs, outputs, and model inputs. That means we can build one reusable component, use it inline, and then open that same component inside a dialog without creating a special wrapper component.&lt;/p&gt;

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

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

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

&lt;p&gt;It needs three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So inline, we can use it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;app-notification-settings&lt;/span&gt;
  &lt;span class="na"&gt;[user]=&lt;/span&gt;&lt;span class="s"&gt;"user()"&lt;/span&gt;
  &lt;span class="na"&gt;[(enabled)]=&lt;/span&gt;&lt;span class="s"&gt;"notificationsEnabled"&lt;/span&gt;
  &lt;span class="na"&gt;(saved)=&lt;/span&gt;&lt;span class="s"&gt;"saveNotificationSettings($event)"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is normal Angular component communication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-04%2Fangular-notification-settings-input-model-output-bindings-inline-component.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-04%2Fangular-notification-settings-input-model-output-bindings-inline-component.png" alt="An example of the notification settings component with an input, model, and output being used inline in the parent template" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;But when we open that same component in an Angular Material dialog, things are different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;openSettingsDialog&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;28rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;panelClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component renders, but it is disconnected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-04%2Fangular-material-dialog-notification-settings-component-disconnected-from-parent.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-04%2Fangular-material-dialog-notification-settings-component-disconnected-from-parent.png" alt="An example of the notification settings component rendered in a dialog, but disconnected from the parent" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Here’s the notification settings component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-notification-settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;MatButtonModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MatSlideToggleModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./notification-settings.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./notification-settings.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationSettingsComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"settings-card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Notification Settings&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

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

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

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

&lt;/div&gt;



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

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

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

&lt;p&gt;It is just a regular Angular component with inputs, a model input, and an output.&lt;/p&gt;




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

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

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

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

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

&lt;p&gt;This lets us bind to the component that gets rendered inside the dialog using &lt;a href="https://angular.dev/api/core/inputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;inputBinding()&lt;/a&gt;, &lt;a href="https://angular.dev/api/core/outputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;outputBinding()&lt;/a&gt;, and &lt;a href="https://angular.dev/api/core/twoWayBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;twoWayBinding()&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;openSettingsDialog&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;28rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;panelClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;outputBinding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveNotificationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nf"&gt;twoWayBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notificationsEnabled&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Each Binding Does
&lt;/h3&gt;

&lt;p&gt;This passes the current user into the dialog component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



&lt;p&gt;This listens for the save event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;outputBinding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveNotificationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



&lt;p&gt;And this keeps the model input synced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;twoWayBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notificationsEnabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

&lt;p&gt;Inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;app-notification-settings&lt;/span&gt;
  &lt;span class="na"&gt;[user]=&lt;/span&gt;&lt;span class="s"&gt;"user()"&lt;/span&gt;
  &lt;span class="na"&gt;[(enabled)]=&lt;/span&gt;&lt;span class="s"&gt;"notificationsEnabled"&lt;/span&gt;
  &lt;span class="na"&gt;(saved)=&lt;/span&gt;&lt;span class="s"&gt;"saveNotificationSettings($event)"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-04%2Fangular-notification-settings-input-model-output-bindings-final-inline-component.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-04%2Fangular-notification-settings-input-model-output-bindings-final-inline-component.png" alt="An example of the notification settings component with an input, model, and output being used inline in the parent template" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And inside a dialog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;28rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;panelClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;inputBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;outputBinding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotificationSettingsSaved&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveNotificationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;twoWayBinding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notificationsEnabled&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-04%2Fangular-material-dialog-notification-settings-input-model-output-bindings.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F06-04%2Fangular-material-dialog-notification-settings-input-model-output-bindings.png" alt="An example of the notification settings component with an input, model, and output being used inside a dialog" width="799" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

&lt;p&gt;Build the component normally, then let the dialog bind to it.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-material-dialog-input-output-model-bindings" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/components/dialog/overview?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Material Dialog Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/inputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular &lt;code&gt;inputBinding()&lt;/code&gt; API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/outputBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular &lt;code&gt;outputBinding()&lt;/code&gt; API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/twoWayBinding?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular &lt;code&gt;twoWayBinding()&lt;/code&gt; API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

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

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

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

&lt;p&gt;We have a support queue where each ticket has a status:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-28%2Fsupport-queue.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-28%2Fsupport-queue.jpg" alt="Support queue showing tickets with New, In Progress, Resolved, and Closed status labels" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are four possible statuses, and we’re modeling them with a TypeScript union:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TicketStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in-progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Ticket&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TicketStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we have a signal containing a list of tickets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;tickets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ticket&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User cannot reset password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme Corp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Billing page shows incorrect total&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Northstar Health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in-progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Exported report is missing rows&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Velocity Labs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1027&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Login button disabled after failed attempt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summit Bank&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1028&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Production checkout error for enterprise account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Atlas Retail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far, this is simple enough.&lt;/p&gt;

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



&lt;p&gt;This works, but there's a subtle problem.&lt;/p&gt;

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

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

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

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

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

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

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

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

&lt;p&gt;We need to support urgent tickets, so we add a new status called "escalated":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TicketStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in-progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resolved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we update one of the tickets to use the new status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1028&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Production checkout error for enterprise account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Atlas Retail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript is happy.&lt;/p&gt;

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

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

&lt;p&gt;But when the escalated ticket renders, the card is incomplete:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-28%2Fescalated-ticket.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-28%2Fescalated-ticket.jpg" alt="Escalated ticket showing the ticket title and customer, but no badge or message" width="800" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;But the template didn't keep up.&lt;/p&gt;




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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

&lt;p&gt;Since &lt;code&gt;TicketStatus&lt;/code&gt; includes &lt;code&gt;escalated&lt;/code&gt;, but the template doesn't handle it yet, Angular reports an error:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-28%2Fescalated-ticket-error.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-28%2Fescalated-ticket-error.jpg" alt="Escalated ticket error showing the template error message" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@case ('new') {
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
@case ('in-progress') {
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@case ('resolved') {
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
@case ('closed') {
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge done"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Done&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;p&gt;Small change, safer template.&lt;/p&gt;

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

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-switch-block-updates" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/api/core/%40switch?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular @switch API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/templates/control-flow?utm_campaign=deveco_gdemembers&amp;amp;utm_source=deveco" rel="noopener noreferrer"&gt;Angular Control Flow Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Better Loading Buttons in Angular Material v22</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 22 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/better-loading-buttons-in-angular-material-v22-17b7</link>
      <guid>https://dev.to/brianmtreese/better-loading-buttons-in-angular-material-v22-17b7</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular Material v22 adds a small but surprisingly useful improvement to buttons: &lt;a href="https://github.com/angular/components/commit/b4a89d5996864e591cfac762db420ec591d931e2" rel="noopener noreferrer"&gt;built-in progress indicator support&lt;/a&gt;. Instead of manually swapping button text with a spinner and dealing with layout jumpiness, we can let the &lt;a href="https://v9.material.angular.dev/components/button/overview" rel="noopener noreferrer"&gt;Material button directive&lt;/a&gt; manage the loading UI for us. In this post, I'll show you the old manual approach, why it creates a small UX issue, and how Angular Material v22 cleans it up.&lt;/p&gt;

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

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

&lt;p&gt;Let's start with a simple reports page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-21%2Freports-page.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-21%2Freports-page.jpg" alt="A simple reports page with a list of reports and a download button for each report" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have a list of reports, and each report has a "Download" button using the &lt;a href="https://material.angular.dev/components/button/overview" rel="noopener noreferrer"&gt;matButton&lt;/a&gt; directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"report-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    @for (report of reports(); track report.id) {
      &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"report-row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"report-info"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"report-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{% raw %}{{ report.name }}{% endraw %}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"report-meta"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{% raw %}{{ report.date }} · {{ report.size }}{% endraw %}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
          &lt;span class="na"&gt;matButton=&lt;/span&gt;&lt;span class="s"&gt;"outlined"&lt;/span&gt;
          &lt;span class="na"&gt;[disabled]=&lt;/span&gt;&lt;span class="s"&gt;"downloadingId() !== null"&lt;/span&gt;
          &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"download(report)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          Download
        &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    }
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

&lt;p&gt;First, we need to import the &lt;a href="https://material.angular.dev/components/progress-spinner/overview" rel="noopener noreferrer"&gt;progress spinner&lt;/a&gt; in our component:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-report-list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;templateUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./report-list.component.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./report-list.component.scss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;changeDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeDetectionStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OnPush&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;MatButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MatProgressSpinner&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReportListComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can update the button template to include the progress spinner conditionally when the report is downloading and the label when it isn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;matButton=&lt;/span&gt;&lt;span class="s"&gt;"outlined"&lt;/span&gt;
  &lt;span class="na"&gt;[disabled]=&lt;/span&gt;&lt;span class="s"&gt;"downloadingId() !== null"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"download(report)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  @if (downloadingId() === report.id) {
    &lt;span class="nt"&gt;&amp;lt;mat-progress-spinner&lt;/span&gt;
      &lt;span class="na"&gt;mode=&lt;/span&gt;&lt;span class="s"&gt;"indeterminate"&lt;/span&gt;
      &lt;span class="na"&gt;[diameter]=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt;
      &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Downloading"&lt;/span&gt;
    &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  } @else {
    Download
  }
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this download doesn't expose a real percentage, &lt;code&gt;mode="indeterminate"&lt;/code&gt; is the right fit here.&lt;/p&gt;

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

&lt;p&gt;And this works fine:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-21%2Freports-page-downloading.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-21%2Freports-page-downloading.gif" alt="A reports page with a list of reports and a download button for each report. When the report is downloading, we show a spinner. When it isn't, we show the label." width="760" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

&lt;p&gt;And we had to write the conditional content logic ourselves.&lt;/p&gt;




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

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

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

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

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

&lt;p&gt;Instead of swapping the label and spinner manually, both can live inside the button at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
  &lt;span class="na"&gt;matButton=&lt;/span&gt;&lt;span class="s"&gt;"outlined"&lt;/span&gt;
  &lt;span class="na"&gt;[showProgress]=&lt;/span&gt;&lt;span class="s"&gt;"downloadingId() === report.id"&lt;/span&gt;
  &lt;span class="na"&gt;[disabled]=&lt;/span&gt;&lt;span class="s"&gt;"downloadingId() !== null"&lt;/span&gt;
  &lt;span class="na"&gt;(click)=&lt;/span&gt;&lt;span class="s"&gt;"download(report)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mat-progress-spinner&lt;/span&gt;
    &lt;span class="na"&gt;progressIndicator&lt;/span&gt;
    &lt;span class="na"&gt;mode=&lt;/span&gt;&lt;span class="s"&gt;"indeterminate"&lt;/span&gt;
    &lt;span class="na"&gt;[diameter]=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt;
    &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Downloading"&lt;/span&gt;
  &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  Download
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important pieces here.&lt;/p&gt;

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

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

&lt;/div&gt;



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

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

&lt;p&gt;Then, the spinner gets the &lt;code&gt;progressIndicator&lt;/code&gt; attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mat-progress-spinner&lt;/span&gt; &lt;span class="na"&gt;progressIndicator&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This marks the spinner as the button's projected progress indicator.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why This Feels Better
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-21%2Freports-page-downloading-with-progress.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-21%2Freports-page-downloading-with-progress.gif" alt="A reports page with a list of reports and a download button for each report. When the report is downloading, we show a spinner. When it isn't, we show the label, this time using the new showProgress input." width="800" height="743"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;h2&gt;
  
  
  Want to Go Deeper With Modern Angular?
&lt;/h2&gt;

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-material-button-progress-indicator" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/components/commit/b4a89d5996864e591cfac762db420ec591d931e2" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/components/button/overview" rel="noopener noreferrer"&gt;Angular Material Button Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://material.angular.dev/components/progress-spinner/overview" rel="noopener noreferrer"&gt;Angular Material Progress Spinner Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Angular v22 WebMCP Tools Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 15 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angular-v22-webmcp-tools-explained-3o9f</link>
      <guid>https://dev.to/brianmtreese/angular-v22-webmcp-tools-explained-3o9f</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular v22 is experimenting with a new way to expose your app’s real capabilities to AI. Instead of letting models guess what's happening by scraping the DOM, we can now provide explicit, state-backed tools directly to the browser. This post walks through setting up &lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; tools to bridge the gap between your Angular state and AI models like &lt;a href="https://gemini.google.com/" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt;.&lt;/p&gt;

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

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

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

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

&lt;p&gt;Here's an example of a basic Angular app with no WebMCP tools registered:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fbasic-angular-app-not-using-webmcp-tools.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fbasic-angular-app-not-using-webmcp-tools.jpg" alt="Basic Angular app not using WebMCP tools" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're using the &lt;a href="https://chromewebstore.google.com/detail/WebMCP%20-%20Model%20Context%20Tool%20Inspector/gbpdfapgefenggkahomfgkhfehlcenpd" rel="noopener noreferrer"&gt;WebMCP Inspector&lt;/a&gt; to see what tools are available and in this case, there are none:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fwebmcp-inspector-empty.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fwebmcp-inspector-empty.jpg" alt="WebMCP Inspector showing no tools available" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;We're using the new &lt;a href="https://dev.to%20post_url%20/2026/04/2026-04-30-angular-service-decorator-injectable-replacement%20"&gt;@Service()&lt;/a&gt; decorator and &lt;a href="https://angular.dev/guide/signals" rel="noopener noreferrer"&gt;signals&lt;/a&gt; to expose profile information and computed business rules.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;selectedUserKeyState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserKey&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sarah&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sarah&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;USER_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sarah&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marcus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;USER_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marcus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;selectedUserKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedUserKeyState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asReadonly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;USER_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectedUserKey&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;USER_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectedUserKey&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;isHighRiskAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysSinceLastLogin&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentFailures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unresolvedPriorityTickets&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;selectUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedUserKeyState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;isHighRiskAccount&lt;/code&gt; signal is exactly the kind of "hidden" logic that an AI wouldn't know about just by looking at the UI. &lt;/p&gt;

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

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

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

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

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

&lt;p&gt;In this case, we register our tools globally in &lt;code&gt;app.config.ts&lt;/code&gt;, but you should also be able to use it in route providers too:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApplicationConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;provideWebMcpTools&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool needs a &lt;code&gt;name&lt;/code&gt;, a &lt;code&gt;description&lt;/code&gt; for the AI to understand when to use it, an &lt;code&gt;inputSchema&lt;/code&gt;, and an &lt;code&gt;execute&lt;/code&gt; function.&lt;/p&gt;

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

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

&lt;p&gt;It's intentionally simple to demonstrate the core wiring: Gemini asks for a summary, and Angular returns the current signal state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_current_user_summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Returns a summary of the current user.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserStore&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Name: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Email: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Plan: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Status: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Last login: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastLogin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Open tickets: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openTickets&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the &lt;code&gt;execute&lt;/code&gt; function directly injects &lt;code&gt;UserStore&lt;/code&gt; and reads the &lt;code&gt;currentUser&lt;/code&gt; signal. &lt;/p&gt;

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

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

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

&lt;p&gt;It leverages the &lt;code&gt;isHighRiskAccount&lt;/code&gt; computed signal we defined earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_retention_risk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Analyzes the current account and explains retention risk.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserStore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isHighRisk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHighRiskAccount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isHighRisk&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;
            &lt;span class="c1"&gt;// Is high risk&lt;/span&gt;
            &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is currently considered high risk.

            Reasons:
            - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysSinceLastLogin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; days since last login
            - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentFailures&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; recent payment failures
            - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unresolvedPriorityTickets&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; unresolved priority ticket

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

            &lt;span class="c1"&gt;// Is low risk&lt;/span&gt;
            &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is currently considered low risk.

            No significant warning signs detected.

            Recommended next action:
            No immediate action needed.`&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This demonstrates how the AI can use app-defined business logic to determine a customer’s risk level, providing actionable insights based on our application's internal state.&lt;/p&gt;

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

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

&lt;p&gt;The message content dynamically changes based on whether the &lt;code&gt;isHighRiskAccount&lt;/code&gt; signal is true or false.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft_customer_success_note&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Drafts a customer success outreach note for the current account.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserStore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isHighRisk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHighRiskAccount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isHighRisk&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; 
            &lt;span class="c1"&gt;// Is high risk&lt;/span&gt;
            &lt;span class="s2"&gt;`Hi &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,

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

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

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

            &lt;span class="c1"&gt;// Is low risk&lt;/span&gt;
            &lt;span class="s2"&gt;`Hi &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,

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

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

            Thanks,
            Customer Success Team`&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tool showcases how the same underlying Angular state can power multiple AI-callable capabilities, leading to context-aware and personalized AI responses.&lt;/p&gt;




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

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

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

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

&lt;p&gt;Once registered, all three tools appear in the WebMCP Model Context Tool Inspector:&lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fwebmcp-inspector-tools.jpg" alt="Gemini using the registered WebMCP tools" width="800" height="639"&gt;

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

&lt;p&gt;We can also see that Gemini already understands the tools available and has filled out the prompt for us:&lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fgemini-understanding-tools.jpg" alt="Gemini understanding the tools available" width="800" height="313"&gt;

&lt;p&gt;When we ask Gemini questions like "Can you summarize this customer account?": &lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fgemini-summarize-customer-account.jpg" alt="Gemini summarizing the customer account" width="800" height="414"&gt;

&lt;p&gt;or "Is this customer at risk?": &lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fgemini-customer-at-risk.jpg" alt="Gemini checking if the customer is at risk" width="800" height="476"&gt;

&lt;p&gt;or "Draft a follow-up message for this customer":&lt;/p&gt;

&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriantree.se%2Fassets%2Fimg%2Fcontent%2Fuploads%2F2026%2F05-14%2Fgemini-draft-follow-up-message.jpg" alt="Gemini drafting a follow-up message for the customer" width="800" height="476"&gt;

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

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

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

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

&lt;p&gt;This is the real value of WebMCP in Angular: safe, explicit, and reactive AI capabilities backed by your existing services and logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Get Ahead of Angular's Next Shift
&lt;/h2&gt;

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

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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/brianmtreese/angular-webmcp-tools" rel="noopener noreferrer"&gt;The source code for this example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/angular/angular/commit/3b0ae5fef0328477ee0f5d51980217e7c583a606" rel="noopener noreferrer"&gt;The commit that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;WebMCP unofficial draft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chromewebstore.google.com/detail/WebMCP%20-%20Model%20Context%20Tool%20Inspector/gbpdfapgefenggkahomfgkhfehlcenpd" rel="noopener noreferrer"&gt;WebMCP / Model Context Tool Inspector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pluralsight.com/courses/angular-styling-applications" rel="noopener noreferrer"&gt;My course "Angular: Styling Applications"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.pluralsight.com/library/courses/angular-practice-zoneless-change-detection" rel="noopener noreferrer"&gt;My course "Angular in Practice: Zoneless Change Detection"&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Angular’s New injectAsync() API Explained</title>
      <dc:creator>Brian Treese</dc:creator>
      <pubDate>Fri, 08 May 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/brianmtreese/angulars-new-injectasync-api-explained-41fh</link>
      <guid>https://dev.to/brianmtreese/angulars-new-injectasync-api-explained-41fh</guid>
      <description>&lt;p&gt;&lt;span&gt;A&lt;/span&gt;ngular v22 just made lazy-loading services much simpler. In this post, we'll explore how to leverage the new &lt;a href="https://github.com/angular/angular/commit/444b024d49725afc8b40aec67cfdb63a1f7f23ea#diff-2e2d6a33b5784aebcbbedeb04abba424b55621fe9ec56d4e44618fdf3d7eedb1" rel="noopener noreferrer"&gt;injectAsync()&lt;/a&gt; API to reduce your main bundle size and replace awkward lazy-loading workarounds. We’ll compare the older manual lazy-loading approach with Angular v22’s new &lt;code&gt;injectAsync()&lt;/code&gt; API and see why the new pattern feels simpler and easier to reason about.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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