# Angular Space > Your Hub for Learning and Growing as an Angular Developer. Reviews of Courses, Books, Workshops, Educational Materials. Audio Podcasts with Experts. Great Discussion & Articles. Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts. Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`). ## Pages ### Hey 👋 - I'm Daniel URL: https://www.angularspace.com/about/ Last updated: 2024-01-07T14:04:10.000Z ### I'm running Angular Space and I have a vision to share! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/01/1500x500--1--1.jpg) --- ### If you don't have the time for The Full Story - TLDR; This is A Space for all Angular Developers. Here is what you get when you visit regularly: 💡 Publicly Available Content - Reviews of Courses, Books, Workshops, Educational Materials. - Recorded Episodes of Angular Space all in one place. - Insightful and unique Articles with my perspective on all things Angular and Tech world in general. - Great Discussion as always. 🏆 Member-only Content (Don't worry it's FREE) - Member only Raffles for FREE E-books, courses, workshops. - Member only Raffles for Discounts on E-books, courses, workshops. - Early Access to Courses, Workshops, Books reviews. - Curated Highlights from X/Twitter discussions (Summary of what the community is talking about with my private commentary) - Get Exclusive Sneak Peek at what's coming. - All of that delivered via E-mail + Members Only Section (If you decide to create Account). ## Get Member-only Content (FREE) Don't worry about Spam. Im not selling anything. I just want to make sure I can deliver content straight to you without any third party services in between. Become a Member Email sent! Check your inbox to complete your signup. 100% value. No spam. Unsubscribe anytime. ### The Full Story I'm an Angular Architect and Nx Champion with around 10 years of commercial experience, known for my expertise in web architecture. As a co-founder of **Angular Bros** and organizer of **Angular Wroclaw**, I am deeply involved in the Angular community. I have been recognized as Angular **Hero of Community in 2023 at NgPoland Conf**. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/01/IMG_7271-1-1.JPG) - My followers base is growing all across the world. On X (Soon 5k). LinkedIn (Soon 5k), Medium (500+), Dev ( 5000+). - Angular Space Community on X has been making crazy numbers currently sitting at 546 members! - I have a bunch of articles written on Medium and Dev. - I have a Live Spaces episodes recordings available on Spotify and Dev. - I have fans scattered across many platforms, it's hard to manage for me - and for you hard to find appropriate content from Angular Space. **angularspace.com** aims to fix this problem while also expanding itself to provide more and better content! ## What's Cooking. I have a lot in store for you. ### Reviews of Courses, Books, Workshops, Educational Materials. This is pretty much self explanatory but in general I decided that I am going to review all kinds of Educational Materials for you. Mainly for Angular so you can decide: - What is worth buying. - What is out there to buy. - If this is what you are looking for. - If the quality is good. - If great value for money requirement is checked. 💰 [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/01/Screenshot-2024-01-07-at-11.05.43.png)](https://angularstart.com/?ref=angularspace.com) First up for review is Course by [Joshua Morony](https://www.youtube.com/@JoshuaMorony?themeRefresh=1&ref=angularspace.com) known for his fantastic YouTube Content. Course is named [Angular Start](https://angularstart.com/?ref=angularspace.com). ### Meet people like you Join a community of other subscribers who share the same interests. ### Membership URL: https://www.angularspace.com/membership/ Last updated: 2024-01-13T03:49:02.000Z _No content available._ ### Signin URL: https://www.angularspace.com/signin/ Last updated: 2024-01-07T13:58:18.000Z _No content available._ ### Subscribe URL: https://www.angularspace.com/subscribe/ Last updated: 2024-01-07T13:58:47.000Z _No content available._ ### Welcome Member! 🤗 🥳 URL: https://www.angularspace.com/welcome-member/ Last updated: 2025-07-29T13:39:43.000Z You have successfully registered as a member of Angular Space. Your membership is for the lifetime of the platform. And will stay completely free forever. 🏆 Here is what your membership UNLOCKS - Member only Raffles for FREE E-Books, courses, workshops. - Member only Discounts on E-Books, courses, workshops. - Curated Highlights from X/Twitter discussions (Summary of what the community is talking about with my private commentary) - Early Access to Courses, Workshops, Books reviews. - Write for us in members section! We will have **Expert Mentors to help you deliver top quality content!** Mentors will teach you how to get better at writing and validate your content quality helping you improve it. - **Exclusive Job Offers Only For Members - life changing opportunity to stand out ! ( Rare but happen from time to time )** - Exclusive Sneak Peek at what's coming. - Exclusive Discord Server - All posts delivered via E-mail + Members Only Posts - \+ much more as new features are kept being developed! ### In Discord we do - OSS Missions - Giveaways - Chat a lot - Ask Technical Questions - Ask Job Related Questions - Post memes - Working on launching Quiz games - We have Angular Hub Bot - We have [](https://www.linkedin.com/in/ACoAAAyUeLcBsp0PomRMVnk9GyGQgMe6sUMNcak?ref=angularspace.com)[Rainer Hahnekamp](https://www.linkedin.com/in/rainerhahnekamp/?ref=angularspace.com) ng-news dedicated channel - We have Angular Challenges by [](https://www.linkedin.com/in/ACoAAAmFNGQBZgbkwKoywpM%5Fgybm-C7ijDhST3s?ref=angularspace.com)[Thomas Laforge](https://www.linkedin.com/in/thomas-laforge-2b05a945/?ref=angularspace.com) dedicated channel - Angular Space Articles dedicated channel - We have points and reputation system ( Gained only by being active and helpful member ) - For points you can buy books from Discord Shop - \+ more DISCORD INVITE [Join the Angular Space Discord Server!Check out the Angular Space community on Discord – hang out with 1632 other members and enjoy free voice and text chat.![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/icon/favicon-36.ico)Discord](https://discord.gg/FYqmxBPp?ref=angularspace.com) Hope you will enjoy your time here! ### Explore URL: https://www.angularspace.com/explore/ Last updated: 2024-01-11T16:50:19.000Z Not sure what you are looking for? Explore! 🗺️ 😄 ### Authors URL: https://www.angularspace.com/authors/ Last updated: 2024-08-20T16:25:22.000Z _No content available._ ### Privacy Policy URL: https://www.angularspace.com/privacy-policy/ Last updated: 2025-01-24T08:00:46.000Z # Privacy Policy \_Last updated: 25th January, 2025 This privacy notice for **AngularSpace.com** ("Company," "we," "us," or "our") describes how and why we might collect, store, use, and/or share ("process") your information when you use our services ("Services"), such as when you: - Visit our website at [https://angularspace.com](https://angularspace.com/?ref=angularspace.com) or any website of ours that links to this privacy notice. - Sign up for our waitlist, newsletters, or updates. - Engage with us in any other related ways, including customer support, events, or collaborations. **Questions or concerns?** Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with our policies and practices, please do not use our Services. If you have any questions, please contact us at [daniel.glejzner@gmail.com](mailto:daniel.glejzner@gmail.com). --- ## Summary of Key Points - **What personal information do we process?** When you visit, use, or navigate our Services, we may process personal information depending on how you interact with us and the Services. - **Do we process any sensitive personal information?** No, we do not collect or process sensitive personal information. - **Do we receive any information from third parties?** We do not currently receive data from third-party sources. - **How do we process your information?** We process your information to provide, improve, and administer our Services, communicate with you, and comply with legal requirements. - **How do we keep your information safe?** We have implemented organizational and technical measures to protect your information. - **What are your rights?** Depending on your location, you may have rights regarding your personal information, including the right to access, update, or delete your data. Want to learn more? Please review the full privacy notice below. --- ## Table of Contents 1. [What Information Do We Collect?](#1-what-information-do-we-collect) 2. [How Do We Process Your Information?](#2-how-do-we-process-your-information) 3. [What Legal Bases Do We Rely On To Process Your Information?](#3-what-legal-bases-do-we-rely-on-to-process-your-information) 4. [When and With Whom Do We Share Your Personal Information?](#4-when-and-with-whom-do-we-share-your-personal-information) 5. [Do We Use Cookies or Tracking Technologies?](#5-do-we-use-cookies-or-tracking-technologies) 6. [How Long Do We Keep Your Information?](#6-how-long-do-we-keep-your-information) 7. [How Do We Keep Your Information Safe?](#7-how-do-we-keep-your-information-safe) 8. [What Are Your Privacy Rights?](#8-what-are-your-privacy-rights) 9. [Do We Make Updates To This Notice?](#9-do-we-make-updates-to-this-notice) 10. [How Can You Contact Us About This Notice?](#10-how-can-you-contact-us-about-this-notice) --- ## 1\. What Information Do We Collect? We collect personal information that you voluntarily provide to us when you: - Sign up for our waitlist or newsletters. - Contact us via email or any other communication channels. The personal information we collect includes: - **Email Address**: For authentication, communication, and updates. - **Other Details**: If you voluntarily provide additional information in communications or forms. We do not intentionally collect sensitive personal data (e.g., health or financial information). --- ## 2\. How Do We Process Your Information? We process your information to: - Provide you with Services, such as access to waitlists and updates. - Send service-related communications (e.g., confirmation emails, updates). - Improve the functionality and security of our website. --- ## 3\. What Legal Bases Do We Rely On To Process Your Information? We process your information under the following legal bases: - **Consent**: When you provide your email address and agree to receive communications. - **Legitimate Interests**: To improve our Services and ensure security. --- ## 4\. When and With Whom Do We Share Your Personal Information? We only share your information with trusted service providers when necessary, including: - **Ghost.org**: For website hosting and email services. - **Plausible Analytics**: For anonymous, cookie-free website analytics. We do not sell or share your personal data for marketing purposes. --- ## 5\. Do We Use Cookies or Tracking Technologies? No, we do not use cookies or similar tracking technologies. For analytics, we use **Plausible Analytics**, which does not use cookies and complies fully with GDPR. --- ## 6\. How Long Do We Keep Your Information? We retain your personal data only for as long as necessary to fulfill the purposes outlined in this policy, unless required by law. --- ## 7\. How Do We Keep Your Information Safe? We implement technical and organizational measures to safeguard your personal information, such as encrypted transmissions and secure servers. However, no electronic transmission is 100% secure. --- ## 8\. What Are Your Privacy Rights? Depending on your location, you may have the following rights under GDPR or other applicable privacy laws: - **Access**: Request a copy of your data. - **Correction**: Update inaccurate information. - **Deletion**: Request deletion of your data. - **Restriction**: Limit how your data is processed. - **Objection**: Object to certain types of processing. To exercise your rights, please contact us at [daniel.glejzner@gmail.com](mailto:daniel.glejzner@gmail.com). --- ## 9\. Do We Make Updates To This Notice? Yes, we may update this notice as necessary to remain compliant with relevant laws. The latest version will always be available on this page. --- ## 10\. How Can You Contact Us About This Notice? If you have any questions or comments about this policy, please contact us: **AngularSpace.com** Email: [daniel.glejzner@gmail.com](mailto:daniel.glejzner@gmail.com). ### test2 URL: https://www.angularspace.com/test2/ Last updated: 2025-03-18T06:13:35.000Z Book a session! Blah Blah [![Book an appointment with Angular Space using Setmore](https://assets.setmore.com/setmore/images/2.0/Settings/book-now-black.svg)](https://danieladxq.setmore.com/?ref=angularspace.com) [![Book an appointment with Angular Space using Setmore](https://assets.setmore.com/setmore/images/2.0/Settings/book-now-black.svg)](https://danieladxq.setmore.com/services/d7afe374-2014-4e9d-b0b1-a6f1660317f9?ref=angularspace.com) ### Angular Talent Registry URL: https://www.angularspace.com/angular-space-talent-registry/ Last updated: 2026-08-11T13:41:36.000Z _No content available._ ## Posts ### Want to Speak at a Conferences? Here’s What I’ve Learned URL: https://www.angularspace.com/want-to-speak-at-a-conferences-heres-what-ive-learned/ Last updated: 2026-09-01T14:00:06.000Z ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/image.png) I still remember my first conference talk. I was nervous and unsure, with a million questions running through my head. Is my topic interesting enough? Will anyone even show up? What if I forget everything I wanted to say? After speaking at ng-India, DevDays Europe, FrontEnd Nation, and many other international conferences, I can say that those concerns never completely disappear. You simply learn how to live with them and turn that nervous energy into something that can actually help you deliver a great talk. ## **Before You Submit Your First Talk** Many people think a conference talk starts when you walk onto the stage. In reality, the real work starts much earlier, when you decide to submit a proposal. And that is where I had to overcome my first major obstacle: the feeling that I had nothing valuable to share. I had spent years working with Angular, solving problems, designing architectures, and leading teams. But somehow, I thought all of that was too obvious and that everyone already knew it. It wasn't until **I started talking to other developers that I realized my "obvious" solutions weren't obvious to everyone else.** Something that has become part of my everyday work could be a real breakthrough for someone else. ## **Choosing a Topic - What Do You Really Want to Say?** ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/image-1.png) When I first started preparing for conferences, I spent a lot of time trying to come up with the right topic. I had several ideas, but none of them felt good enough. Eventually, I realized that the best talks usually come from real experience. You don't need to come up with something revolutionary. You need to share something you have actually experienced and learned from. Most of the topics I chose came directly from problems I had faced in my day-to-day work. They were things I could talk about for hours because I knew the details, the pitfalls, and the solutions inside out. That's my first piece of advice: > *choose topics you genuinely know, rather than topics that simply seem trendy or popular.* ## **The Call for Papers** Most conferences have a Call for Papers, or CFP, where speakers can submit their talk proposals. Usually, you need to describe your topic, provide an abstract, and often include some information about yourself. Popular international conferences can receive hundreds of submissions, so your proposal needs to stand out. I've learned that a good abstract isn't just a dry description of what you're going to talk about. It's a **promise of value to the audience**. Instead of saying, > "I'll talk about state management in Angular," you could say, > "I'll show you how to avoid three common state-management mistakes that cost my team weeks of debugging." The difference may seem small, but it's significant. In the second version, the audience immediately understands what they will get out of the talk. It's also worth remembering that conference organizers are looking for variety. Not every talk needs to be about the latest framework features. Sometimes a talk about successfully migrating legacy code can be much more valuable than yet another presentation about signals or standalone components. ## **Preparing the Presentation - Less Is More** ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/image-2.png) When I got confirmation that my proposal for my first international conference had been accepted, I felt a mixture of excitement and panic. Suddenly, I had a deadline and had to create something that would be worth the attention of hundreds of people. That's when I made one of the most common mistakes new speakers make: I tried to fit everything I knew into a single presentation. My first slides were packed with text, diagrams, and code examples. I wanted to show how much I knew. Feedback from more experienced speakers eventually made me realize that this wasn't going to work. People simply **can't absorb that much information** in forty-five minutes. It's better to say less, but make sure people actually remember it. So I started again. I chose three main ideas that I wanted people to take away and built the entire presentation around them. Every slide had to support one of those ideas. If it didn't, it was gone. It was painful because I had to remove many things that I thought were important, but the final result was much stronger. ## **Slides - Visual Support, not a Teleprompter** One of the most important lessons I learned while preparing for conferences was understanding what slides are actually for. **They aren't there for you to read from.** They're there to support your message visually. If everything you're saying is already on the slide, **why would people need to listen to you?** They could just read it themselves. I started following a simple rule: **one sentence or one diagram per slide.** Sometimes, just one word. This forced me to explain things in my own words and connect with the audience instead of staring at the screen and reading. Code on slides is a separate challenge. At FrontEnd Nation, I had a presentation with lots of code examples. I learned that **code on a slide needs to be minimal:** only show what is absolutely necessary to understand the concept. Everything else becomes noise. It's much better to show five lines of code and explain them properly than fifty lines that people quickly scan and forget. ## **Practice, Practice, and More Practice** ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/image-3.png) There is no such thing as too much practice. Before a conference, I usually run through my presentation many times. I practice out loud, in front of a mirror, while recording myself, with my family, and with colleagues. Every time, I discover something new. Maybe I stumble over a sentence, lose my train of thought, speak too quickly, or realize that an example isn't clear enough. Recording yourself is particularly useful and, at the same time, painful. Nobody likes listening to their own voice, but it's one of the best ways to spot your own habits. I discovered that I had a tendency to say "kind of" and "basically" in almost every other sentence. Once I heard myself doing it, I could finally start working on it. I also strongly recommend speaking at a smaller event, such as a meetup, before presenting at a large international conference. The audience is usually more relaxed about mistakes, there are fewer lights and cameras, and the whole experience tends to be more informal. It gives you a chance to see how the talk works with a real audience and a live Q&A. It's also important to practice with a timer. Conferences have strict time limits, and going over your allotted time is unprofessional. At FrontEnd Nation, for example, I had forty-five minutes and needed to finish on time. That takes practice. You need to know how long each section of your presentation takes. ## **The Day Before - Logistics and Staying Calm** The day before my conference, I spent a lot of time checking everything over and over. Did I have the presentation on my laptop? Did I pack the charger? Did I have a backup? Did I know where the room was? Did I know what time I needed to be there? It might sound excessive, but trust me: **you don't want to discover a technical problem five minutes before going on stage.** I've seen speakers deal with projectors that didn't work, laptops that died, or presentations that suddenly disappeared. Most of these problems could have been avoided with a little preparation. I also try not to work on the presentation the day before the talk. If it's not ready by then, staying up late for a few extra hours isn't going to save it. It's much better to get some sleep and arrive on stage fresh than to stay up until three in the morning tweaking slides. ## **On Stage - The First Few Minutes Matter** I still remember walking onto the stage at DevDays Europe. Hundreds of people, bright lights, a microphone, and a heart that was beating like crazy. Then I remembered advice from a more experienced speaker: > the first thirty seconds set the tone for everything. If you start confidently, the rest tends to follow. If you start uncertainly, you can end up fighting that feeling throughout the entire presentation. That's why I always practice my opening lines until I know them inside out. I don't improvise at the beginning. I know exactly what I'm going to say, how I'm going to say it, and where I'm going to stand. It gives me a sense of control when the stress is at its highest. After the first minute or so, the adrenaline starts working for me rather than against me. Eye contact with the audience is another thing I had to learn. The natural reaction is to look at the slides or stare at a single point on the wall. But a **presentation is still a conversation with people**, even if it's mostly one-way. I try to look at different people in different parts of the room and make brief eye contact. It makes the presentation feel much more personal. ## **Dealing with Questions** The Q&A after a presentation used to be one of the most stressful parts for me. What if someone asks a question I don't know the answer to? What if someone challenges my assumptions? Over time, I learned that this is simply part of the process, and there's nothing wrong with saying, > "I don't know, but I'll look into it and get back to you." The worst thing you can do is pretend to know something when you don't. People can usually tell, and you lose credibility. It's better to be honest. Being an expert doesn't mean knowing everything. It means knowing what you don't know and being comfortable admitting it. ## **After the Talk - Networking and Feedback** ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/image-4.png) A conference talk isn't just about the presentation itself. It's also an opportunity to connect with other developers, speakers, and organizers. After my talks, people would often come up to me with questions, comments, and their own experiences. Those conversations were often just as valuable as the presentation itself. I also always **ask for feedback**. Not everyone will give it, but when they do, it can be incredibly useful. Maybe you speak too quickly. Maybe your examples were too complicated. Maybe the audience needed more context. Every piece of feedback helps you become a better speaker the next time. ## **Rejections Are Part of the Game** Not every talk I've submitted has been accepted. I've received plenty of rejections, sometimes without any explanation. At first, I took them personally. Over time, I realized that rejection is simply part of the process. Conferences have a limited number of slots and have to choose from many strong proposals. A rejection doesn't necessarily mean your topic is bad. It may simply not have been the right fit for that particular conference. The key is consistency. Apply to multiple conferences, **don't let rejections discourage you**, and learn from every submission. With every application, you get better at writing abstracts, choosing topics, and presenting yourself. ## **Is It Worth It?** People sometimes ask me whether it's really worth spending so much time preparing for conference talks. After all, we're talking about dozens of hours of work, stress, and travel. My answer is always the same: **absolutely**. Speaking at international conferences has opened doors I didn't even know existed. I've met amazing people, built collaborations that are still going today, and developed a stronger presence in the developer community. All of that has had an impact on my career in ways I could never have predicted. But even without those external benefits, preparing a presentation is incredibly valuable in itself. It forces you to organize your knowledge, rethink things that once seemed obvious, and look at a problem from the perspective of someone who has never encountered it before. It's an **exercise that makes you a better expert**, whether or not you ever step onto a big stage. ## Final Thoughts If you're wondering whether you should try speaking at a conference, give it a shot. Start with smaller meetups, internal presentations at work, or lightning talks. Every talk gives you experience and brings you one step closer to bigger stages. You don't have to be perfect the first time. Nobody is. I remember my own awkward first attempts and compare them with how I feel on stage today. The difference is huge, but it didn't happen overnight. It came from dozens of talks, hundreds of hours of preparation, and thousands of mistakes that I made, learned from, and fixed. You can take the same journey. **You just have to start.** ### The best API integration platforms for SaaS apps and agents in 2026 URL: https://www.angularspace.com/best-api-integration-platforms-for-saas-and-agents/ Last updated: 2026-08-28T13:54:46.000Z *A hands-on guide for Angular and TypeScript teams.* Let's say you've built a B2B SaaS product and your first enterprise prospect asks, "Does it integrate with Salesforce?" The next prospect asks about HubSpot, and the one after that about NetSuite. You can build each integration in-house, but every one takes engineering time away from the product. Building integrations in-house means handling OAuth flows, token refresh, rate limits, pagination, and webhooks separately for every API your customers use. The alternative is an embedded integration platform: infrastructure you ship inside your product that lets each customer connect their own tools from your settings page. I compared six platforms by reviewing their SDKs and documentation. I focused on how easily each fits into an Angular app and how much backend control you retain when an integration goes beyond the standard flow. ## What's the best SDK or API for embedding third-party app integrations into a SaaS product? Nango is best for engineering teams building deep, custom integrations. It has a pre-built catalog of 900+ APIs. Engineers write the integration logic in code and can customize it for each API. Nango Cloud runs these integrations at enterprise scale, while self-hosting and BYOC (bring your own cloud) are available on an Enterprise plan. However, Merge fits teams that need unified APIs with fixed data models, rather than deep, custom object access in their APIs. Paragon and Prismatic let teams build workflows visually. Apideck provides real-time unified APIs across seven categories. Pipedream Connect requires less setup for quick prototypes and long-tail APIs. ## TL;DR - **Nango:** Best for engineering teams that need a broad API catalog and full control over their integration logic. - **Merge:** Best for teams that need unified APIs with fixed data models, rather than deep, custom object access. - **Paragon:** Best for non-technical teams that prefer low-code visual workflows for building integrations, though lower plans have stricter execution limits. Prismatic is designed around embeddable workflows. Apideck provides broad unified API coverage, while Pipedream Connect is useful for prototypes and less common APIs. See the [comparison table](#comparison-of-embedded-integration-platforms) for a side-by-side summary. ## What is an embedded integration platform? An embedded integration platform (also called an embedded integration platform as a service, or embedded iPaaS) adds customer-facing account connections to your product. A customer can connect Salesforce from your settings page while the platform handles OAuth, credential storage, token refresh, retries, and rate limits behind the scenes. Zapier and Make are built for people automating their own accounts. An embedded platform instead helps a SaaS company offer integrations as part of its product. ## What is a unified API and how does it work? A unified API exposes one schema across several providers in the same category. Your app calls the vendor's contact endpoint, for example, and the vendor translates that request for Salesforce, HubSpot, or Pipedrive. That shared schema works well for standard fields. Custom fields, provider-specific settings, and objects that do not map cleanly usually require field mapping, raw data, or passthrough requests. The available options vary by vendor and plan. ## Native integrations vs unified APIs vs embedded iPaaS: when each fits Most teams choose one of three approaches: - **Native integrations built in-house:** building natively gives you full API access, but your team owns [authentication](https://nango.dev/blog/why-is-oauth-still-hard?ref=angularspace.com), token refresh, rate limits, and ongoing maintenance for each provider. Sensible when you need only one or two integrations and they are your core product. - **Unified APIs (Merge, Apideck):** fastest path to covering a category with standard data. The fixed schema works until a customer asks for a custom field, a provider-specific endpoint, or a category the vendor does not cover. - **Embedded integration platforms (Nango, Paragon, Prismatic, Pipedream Connect):** the platform handles auth and infrastructure across a large catalog, while integration logic stays yours, either as code or as visual workflows depending on the platform. Choose an embedded integration platform when you need more control than a unified API provides but do not want to build the authentication and execution infrastructure yourself. Some vendors fall into more than one of these categories. Ask whether standard categories and fields will still cover your needs a year from now. If they will, a unified API is usually the quickest route. If customers are likely to need provider-specific behavior, choose a platform that leaves the integration logic under your control. ## How we evaluated these platforms I evaluated each platform using eight criteria: 1. **Developer experience and control:** code-first or low-code, TypeScript support, and whether engineers can customize any part of an integration or hit a wall. I also looked for a local development workflow and for abstractions you can drop below when a requirement does not fit. 2. **Frontend embedding quality:** the actual SDK story, meaning the auth and connect UI components each platform ships, any headless option, how the OAuth popup and token exchange work, and whether it all works cleanly in an Angular app or is React-only. This is where the platforms differ most for Angular teams: most ship framework-agnostic SDKs, but some reserve their official packages or pre-built UI for React and Vue. 3. **Depth on complex systems of record:** Salesforce, HubSpot, NetSuite, Workday, Zendesk, and the custom fields and schemas that come with them. The differences show up in custom fields and per-customer schemas rather than in standard objects. 4. **Catalog breadth:** how many APIs ship pre-built, and what happens when the one you need is missing. A large catalog only matters if there is also a documented path for the API it doesn't cover. 5. **Auth and credential handling:** where tokens are stored, how they refresh, and whether credentials ever touch the browser. 6. **Deployment and compliance basics:** SOC 2, GDPR, hosting regions, and self-hosting or BYOC (deploying in your own cloud). 7. **Pricing transparency:** public pricing versus "book a demo". Whether billing is per connection or usage-based also determines how costs grow with your customer base. 8. **Open source:** whether you can audit the platform and keep a path out of vendor lock-in. A self-host path means you can keep running even if the vendor's pricing or roadmap changes. ## The best SDKs and platforms for embedding third-party integrations in 2026 ### 1\. Nango **Overview** [Nango](https://nango.dev/?ref=angularspace.com) pairs a catalog of 900+ pre-built APIs with integration logic you keep in your own repository. The platform handles authentication, the connect UI, token refresh, and proxying; you write product-specific syncs, actions, and webhook handlers as functions. The core platform is [open source](https://github.com/NangoHQ/nango?ref=angularspace.com) and self-hostable. Nango's [building-nango-functions skill](https://nango.dev/docs/getting-started/coding-agent-setup?ref=angularspace.com#skills) lets Cursor, Codex, Claude Code, and other coding agents write those functions, test them with `nango dryrun` against a live connection, and deploy them. [Pricing](https://nango.dev/pricing?ref=angularspace.com) is public and usage-based with a free tier; metered charges include connections plus platform usage such as proxy requests and function compute. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/NangoAPIs.gif) **Best for** Engineering teams building deep, custom integrations that need a broad pre-built catalog while keeping their integration logic in code. **Pros** - **Pre-built catalog of 900+ APIs:** the [catalog](https://nango.dev/api-integrations?ref=angularspace.com) covers providers using API keys, basic auth, OAuth 2.0, OAuth 1.0a, and custom provider-specific schemes, with a drop-in connect UI and automatic token refresh. - **SDKs for frontend and backend:** the `@nangohq/frontend` SDK is framework-agnostic TypeScript that plugs directly into an Angular service, and the `@nangohq/node` backend SDK covers session tokens, proxy requests, and webhook verification. - **White-label connect UI:** the connect UI can run under your product's branding, and the headless `nango.auth()` path lets you keep your own design system entirely. - **Management MCP tools:** the [Management MCP server](https://nango.dev/docs/updates/changelog?ref=angularspace.com#new-management-mcp-tools) exposes 14 tools, so coding agents can configure integrations, inspect connections, call external APIs, and search the Nango docs directly. - **Deployment and compliance:** [SOC 2 Type II and GDPR compliance, with a HIPAA BAA on request](https://trust.nango.dev/?ref=angularspace.com), tenant isolation at scale, and an Enterprise option to run the full platform in your own cloud account and region, self-hosted or BYOC. - **Code-first development:** integrations live as functions in your repo, so engineers can implement custom fields, objects, and per-customer rules for systems like Salesforce or NetSuite directly in TypeScript. `nango dryrun` tests functions against real connections, the CLI can generate tests, and platform operations produce logs, with OpenTelemetry export of sync, action, webhook, and proxy executions on the plans that include it. **Cons** - **More upfront work than a fixed unified API:** the builder skill can generate a starting point, but your team still owns the data models and mappings. - **The free self-hosted edition is limited:** it targets lightweight deployments that need auth and the proxy, without functions, webhooks, or the platform's managed features; self-hosting the full platform requires an Enterprise plan. **Angular integration notes** `@nangohq/frontend` is plain TypeScript with no framework dependency. You can call it directly from an Angular service. Your backend mints a short-lived session token, the frontend opens the connect UI, and with the proxy or functions handling provider calls, OAuth access and refresh tokens do not need to enter the Angular app or your backend. The full worked example below uses it. ### 2\. Merge **Overview** [Merge](https://merge.dev/?ref=angularspace.com) normalizes 240+ integrations into common data models through category-specific unified APIs, including HRIS, ATS, accounting, CRM, ticketing, file storage, knowledge base, and chat. Your customers authenticate through Merge Link, and you consume one API per category. A separately sold Agent Handler product exposes its connectors to AI agents as MCP tools. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/69775512e7a6e71dbb938124_68e52c1b87224a9ceebdca7e_66bb079fbdbd949699c23e61_66bb062156980bdaa4b29477_merge.png) **Best for** Teams that need unified APIs with fixed data models, rather than custom object access in their APIs. **Pros** - **Fast category coverage:** one implementation gives you access to multiple providers through the category's common models, which are well established for HRIS and ATS. - **Embedded auth options:** Merge Link handles the connect UX, and a hosted [Magic Link](https://docs.merge.dev/merge-unified/merge-link/magic-link?ref=angularspace.com) variant needs no frontend code at all. - **Compliance and regional hosting:** [ISO 27001 and SOC 2 Type II certifications, plus HIPAA and GDPR](https://merge.dev/security?ref=angularspace.com), with US, EU, and APAC hosting options. **Cons** - **The common model constrains you:** field mapping, Remote Data, custom objects, and authenticated passthrough can cover some gaps, but access varies by feature and plan: all four require at least the contract-priced Professional plan, and programmatic field mapping is Enterprise-only. Passthrough requests still require provider-specific API calls. - **You cannot extend the unified API yourself:** there is no way to add an unsupported provider to Merge's unified APIs on your own. Agent Handler can [register a remote MCP server as a custom connector](https://docs.merge.dev/merge-agent-handler/build/connecting-agents/custom-mcp-servers?ref=angularspace.com), but you host and operate that server yourself. - **Faster syncs require higher plans:** the first three production linked accounts are free; the Launch plan then costs [$650 per month for up to 10 linked accounts, with additional accounts at $65 each](https://merge.dev/pricing?ref=angularspace.com), and syncs daily. Customizable sync frequency and additional customization features sit on the contract-priced Professional and Enterprise plans. **Angular integration notes** Merge ships official React and Vue packages, but no Angular one. From Angular you load their CDN script (`cdn.merge.dev/initialize.js`) and drive the global `MergeLink` object yourself: call `MergeLink.initialize` with a backend-created link token, open the modal with `MergeLink.openLink()`, then exchange the returned public token server-side. You must provide the TypeScript definitions and Angular lifecycle handling yourself. ### 3\. Paragon **Overview** [Paragon](https://www.useparagon.com/?ref=angularspace.com) uses a visual workflow engine for embedded integrations. You compose integrations from pre-built steps across 130+ connectors, embed the customizable Connect Portal for customer auth, and optionally express workflows in code through Paragraph, its TypeScript framework, which syncs back into the visual builder. For AI products, ActionKit gives agents 1,000+ pre-built actions. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/paragon-visual-workflow-builder.png) **Best for** Teams that would rather assemble integrations from pre-built workflow blocks than write them as code, with product or solutions engineers doing much of the building. **Pros** - **Embedded auth options:** the Connect Portal is customizable, and a headless mode lets you keep your own design system while Paragon handles auth. - **Pre-built tools for agents:** ActionKit includes a tool catalog, an MCP server, and triggers for agent tool calls. - **Flexible deployment:** cloud with US and EU regions, or self-hosted in your own AWS, Azure, or GCP account on the Enterprise plan, plus [HIPAA, GDPR, SOC 2 Type II, and ISO 27001](https://security.useparagon.com/?ref=angularspace.com). **Cons** - **Custom logic stays inside the workflow model:** code written with Paragraph still runs as a Paragon workflow, subject to plan-based workflow and concurrency limits, and its JavaScript Function steps are restricted to supported npm modules. - **Tight limits outside Enterprise:** function steps [cap at 1 minute of execution](https://docs.useparagon.com/workflows/functions?ref=angularspace.com) on Trial, Basic, and Pro plans, with [20 concurrent step executions](https://docs.useparagon.com/billing/concurrency-limits?ref=angularspace.com) on Pro. - **No public pricing:** no dollar figures are published; quotes are based partly on connected users. **Angular integration notes** `@useparagon/connect`, Paragon's frontend JavaScript SDK, works directly from Angular: sign a JWT for the user on your backend, call `paragon.authenticate(projectId, jwt)`, then `paragon.connect('salesforce')` to open the portal. Headless mode gives Angular teams full control over the connect UI. ### 4\. Prismatic **Overview** [Prismatic](https://prismatic.io/?ref=angularspace.com) supports two ways to build integrations: a low-code visual designer and code-native TypeScript projects built with its open-source Spectral library and `prism` CLI. It can also embed an integration marketplace and an end-user workflow builder in your product. The docs catalog lists 200+ built-in components, and connector source code is public. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/prismatic-1.jpg) **Best for** Teams needing a visual designer for non-engineers or end customers, plus a TypeScript option for engineers. **Pros** - **Embeddable marketplace and workflow builder:** end customers can browse, activate, and even build integrations inside your product. - **Broad deployment options:** [US, Europe, Canada, Sydney, Cape Town, AWS GovCloud](https://prismatic.io/docs/configure-prismatic/deployment-regions/?ref=angularspace.com), or a dedicated deployment in your own AWS account, with SOC 2 Type II plus HIPAA and GDPR. - **SDK for programmatic control:** the Spectral SDK and `prism` CLI let engineers provision the UI-based flows programmatically, with the option to add custom scripts where a pre-built component falls short. **Cons** - **Embedded screens use hosted iframes:** the default marketplace and workflow-builder embeds use hosted iframes. You can build a custom marketplace with the SDK, but the hosted screens offer less styling control. - **Runtime limits:** 15 minutes maximum execution per run and 1 GB of memory by default (raisable to 10 GB), with requests rejected (HTTP 429) once plan concurrency is hit ([runner limits](https://prismatic.io/docs/integrations/integration-runner-environment-limits/?ref=angularspace.com)). - **Syncs are flow-based rather than a normalized managed-sync API:** Prismatic supports large data syncs natively in code-native integrations and through batched custom triggers in the low-code designer and embedded workflow builder, alongside persisted cross-execution state and cursors. You still build the sync from flows and triggers rather than consume a sync API. Pricing is sales-led, with the embedded workflow builder starting at the Enterprise tier on the [pricing page](https://prismatic.io/pricing/?ref=angularspace.com). **Angular integration notes** `@prismatic-io/embedded` is framework-agnostic (npm or a UMD script tag). You authenticate with an RS256 JWT from your backend, then point `prismatic.showMarketplace({ selector: '#placeholder' })` at a DOM element in your component's template. Angular's view encapsulation does not reach into the iframe, so theming happens through Prismatic's config rather than your styles. ### 5\. Apideck **Overview** [Apideck](https://www.apideck.com/?ref=angularspace.com) is a unified API vendor that sends requests to providers in real time instead of syncing records into a vendor-side cache. It covers 200+ connectors across its unified API categories, including accounting, CRM, HRIS, e-commerce, ATS, file storage, and issue tracking. Customers authenticate through Vault, which ships as a hosted page or an embeddable modal. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/69ab8811ea53bf3977f2aecc_698d29b02d5bf88aac360057_66bb71495ea6629c48a32804_66bb7038f58fb1c4b6848e71_apideck.png) **Best for** Teams that want unified API coverage across several categories and prefer real-time requests over a vendor-managed record cache. **Pros** - **Public, self-serve pricing:** [pricing](https://www.apideck.com/pricing?ref=angularspace.com) is public and based on active consumers, with unlimited connections per consumer. - **Proxy on every plan:** raw passthrough requests to supported providers are available on every plan. - **Zero data retention for business data:** customer records and API payloads are not persisted. Encrypted OAuth credentials and optional log metadata are stored, and on-prem connectors like QuickBooks Desktop are a documented exception. **Cons** - **Uneven depth across categories:** as of August 2026, the [connector directory](https://www.apideck.com/integrations?ref=angularspace.com) lists five file storage and nine issue tracking connectors versus 53 for accounting and 59 for HRIS, so check that the providers and data models you need are available. - **No managed record cache or sync engine:** for workflows beyond real-time request-response calls, your team must store records and track sync state, though Apideck's native and virtual [webhooks](https://developers.apideck.com/guides/webhooks?ref=angularspace.com) help with change detection. - **Enterprise-only branding and SSO:** removing Apideck branding from Vault and SSO both require the Enterprise plan. **Angular integration notes** `@apideck/vault-js` is vanilla JavaScript and straightforward to use from Angular: create a session server-side with a `session_length` you choose (one hour by default, up to one week), then `ApideckVault.open({ token })` from any component. Callbacks such as `onConnectionChange` can update Angular signals directly. ### 6\. Pipedream Connect **Overview** [Pipedream Connect](https://pipedream.com/docs/connect?ref=angularspace.com) brings Pipedream's managed authentication, API proxy, and workflow tools into customer-facing products, with [10,000+ built-in API operations across 3,000+ APIs](https://pipedream.com/docs/connect/components?ref=angularspace.com). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/69ab949e1f8dc889aaaee357_699756fde6b77da9c463530e_699753a8c1aacae60907e5cd_6995e85bca9ae3d90ccc7896_PipedreamConnectWorkflows.png) **Best for** Fast prototyping, internal automations, and long-tail APIs that may be missing from smaller catalogs. **Pros** - **3,000+ apps in the catalog:** the catalog spans more than 3,000 apps, and its components are source-available. - **Public, self-serve pricing:** development is free, and production billing is based on compute credits plus the number of unique external users ([pricing docs](https://pipedream.com/docs/pricing?ref=angularspace.com)). - **MCP support:** the same catalog powers an MCP server exposing thousands of tools. **Cons** - **Action configuration is multi-step:** unless you use `@pipedream/connect-react`, running a pre-built action means first retrieving and configuring its properties through a series of API calls, which adds frontend work to an embedded flow. - **No managed sync engine:** to build a sync, you combine [deployable pre-built triggers](https://pipedream.com/docs/connect/components/triggers?ref=angularspace.com), workflows, and your own storage. Custom tools can be published on the Business plan, but publishing your own custom triggers is [listed as coming soon](https://pipedream.com/docs/connect/components/custom-tools?ref=angularspace.com) as of August 2026\. Hard limits apply, such as a 30-second proxy timeout. - **No self-hosted Connect:** Pipedream does not document a self-hosted option for Connect; hosting is Pipedream's managed cloud on AWS in the us-east-1 region ([privacy and security docs](https://pipedream.com/docs/privacy-and-security?ref=angularspace.com)). **Angular integration notes** The core `@pipedream/sdk` runs in any browser app, so the token flow and the client's `connectAccount()` method work from Angular. But Pipedream's pre-built UI components for configuring actions ship only as `@pipedream/connect-react`. An Angular team either rebuilds those prop-configuration forms by hand or works with the core SDK directly. ## Worked example: adding a "Connect your CRM" flow to an Angular app For this example, let's use Nango's SDK to build the integrations page. The browser receives only a short-lived session token, which Nango expires after 30 minutes. Nango handles OAuth through its connect UI, while your server uses the connection ID for proxy calls or functions. Provider access and refresh tokens never need to enter the Angular app or your backend. Let's start with the backend. `server/integrations.ts` creates the Nango client and exposes an endpoint that mints a session token for the signed-in user. The client takes the webhook signing key up front, because the same file verifies webhooks further down: ```typescript import { Nango } from '@nangohq/node'; const nango = new Nango({ apiKey: process.env.NANGO_API_KEY!, webhookSigningKey: process.env.NANGO_WEBHOOK_SIGNING_KEY!, }); app.post('/api/integrations/session', async (req, res) => { const { data } = await nango.createConnectSession({ tags: { end_user_id: req.user.id, organization_id: req.user.orgId }, allowed_integrations: ['salesforce'], }); res.json({ sessionToken: data.token }); }); ``` The endpoint returns one thing: a session token scoped to this user and to the Salesforce integration. That token is all the frontend ever sees. Next, we create an Angular service for the connect flow and expose the connection state as a signal. `@nangohq/frontend` has no framework dependency, so no wrapper is needed. We use `fetch` here to keep the example small; in a real app you would call this endpoint through `HttpClient` so your auth interceptor covers it: ```typescript import { Injectable, signal } from '@angular/core'; import Nango from '@nangohq/frontend'; @Injectable({ providedIn: 'root' }) export class IntegrationsService { readonly crmStatus = signal<'disconnected' | 'connecting' | 'connected'>('disconnected'); async connectCrm(): Promise { this.crmStatus.set('connecting'); try { const res = await fetch('/api/integrations/session', { method: 'POST' }); if (!res.ok) throw new Error(`Session request failed: ${res.status}`); const { sessionToken } = await res.json(); new Nango().openConnectUI({ sessionToken, onEvent: (event) => { if (event.type === 'connect') this.crmStatus.set('connected'); if (event.type === 'error') this.crmStatus.set('disconnected'); if (event.type === 'close' && this.crmStatus() === 'connecting') this.crmStatus.set('disconnected'); }, }); } catch { this.crmStatus.set('disconnected'); } } } ``` The service requests a token, opens Connect UI, and tracks its events in the `crmStatus` signal. The `close` check resets the state if the user closes Connect UI before finishing. The provider's own OAuth page opens separately; pass `detectClosedAuthWindow: true` if you also want that popup's early closure treated as a failed authorization. Finally, the settings page component. Because the state is a signal, the `onEvent` callback updating it from outside Angular's control flow still renders correctly in a zoneless app: ```typescript import { Component, inject } from '@angular/core'; import { IntegrationsService } from './integrations.service'; @Component({ selector: 'app-integration-settings', template: ` @switch (integrations.crmStatus()) { @case ('connected') {

Salesforce is connected.

} @case ('connecting') {

Waiting for authorization...

} @default { } } `, }) export class IntegrationSettingsComponent { protected readonly integrations = inject(IntegrationsService); } ``` That is all the frontend code required for this example. Everything in it is standard modern Angular, and it runs unchanged on any currently supported version. After OAuth completes, Nango sends your backend a webhook containing the new connection ID. Configure a webhook URL and enable connection-creation webhooks in the environment settings first. Before using the payload, verify the `X-Nango-Hmac-Sha256` signature. Verification needs the raw request body, exactly as Nango sent it, so register this route with `express.raw()` before any global `express.json()` middleware. The `nango` client from the first snippet already has the signing key: ```typescript app.post('/api/webhooks/nango', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body.toString('utf8'); if (!nango.verifyIncomingWebhookRequest(rawBody, req.headers)) return res.sendStatus(401); const body = JSON.parse(rawBody); if (body.type === 'auth' && body.operation === 'creation' && body.success) { // Persist body.connectionId against the user in body.tags.end_user_id } res.sendStatus(200); }); ``` After that, your server can read CRM data through the proxy or run syncs for that connection ID. The [Nango quickstart](https://nango.dev/docs/getting-started/quickstart?ref=angularspace.com) is the shortest path to trying this flow yourself. The `connect` event in the UI is for optimistic state; the webhook is the source of truth. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/nango-connect-angular-settings-page.png) *Nango Connect UI opened from the Angular settings page using a short-lived session token.* ### The same flow on the other five platforms Each platform keeps provider credentials out of the Angular app, but the browser credential differs: some SDKs take a vendor-issued session or link token, while Paragon and Prismatic take a JWT your backend signs. What changes beyond that is the packaging, and how much UI you have to rebuild: | | Connect UI | Headless option | Your backend mints | Frontend packages | | ----------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------------- | | Nango | Connect UI (iframe modal); provider OAuth in a popup | Yes, via nango.auth() | Session token (30 min) | @nangohq/frontend, framework-agnostic | | Merge | Merge Link modal | No; hosted Magic Link covers the no-code case instead | Link token | React and Vue packages, CDN script otherwise | | Paragon | Connect Portal modal | Yes, headless Connect Portal | JWT you sign | @useparagon/connect, framework-agnostic | | Prismatic | Marketplace and workflow-builder iframes | Partial: custom marketplace UI over its API | RS256 JWT you sign | @prismatic-io/embedded, framework-agnostic | | Apideck | Vault modal | Partial: build your own UI on the Vault API | Vault session token (1 hour default) | @apideck/vault-js, vanilla JS, plus React and Vue wrappers | | Pipedream Connect | Pipedream iframe; provider OAuth in a popup | Partial: you control the surrounding UI, but auth uses Pipedream's iframe | Connect token (4 hours) | @pipedream/sdk core; action-config UI is React-only | Two mechanics matter in an Angular app regardless of vendor. The first is popup blockers: browsers only allow `window.open` inside a user-gesture call stack. The modal and iframe SDKs sidestep this, because the provider's authorization popup opens from the user's click inside the vendor's surface, not from your code. A headless call such as `nango.auth()` opens the popup from your call stack instead, so awaiting a token request between the click and the SDK call can get the popup blocked in stricter browsers. In that case, mint the token when the settings page loads and call the SDK synchronously from the click handler. Nango surfaces this failure as a typed `blocked_by_browser` error you can react to. The second is server-side rendering. These frontend connection flows use browser APIs, so in an Angular SSR app, construct and call them from event handlers, as the service above does, or behind `afterNextRender`. ## Comparison of embedded integration platforms Use the table as an embedded iPaaS comparison at a glance; the two unified API vendors are included for contrast. | | Nango | Merge | Paragon | Prismatic | Apideck | Pipedream Connect | | ------------------------- | -------------------------------------------- | ----------------------------------- | ----------------------------- | ---------------------------- | ---------------------------------------------------------------- | -------------------------- | | Model | Code-first platform | Unified API | Low-code iPaaS | Low-code + code iPaaS | Unified API (real-time) | Embedded workflows | | Catalog | 900+ APIs | 240+ integrations | 130+ connectors | 200+ components | 200+ connectors | 3,000+ apps | | Angular-friendly SDK | Yes (plain TS) | CDN script only | Yes (plain JS) | Yes (iframe-based) | Yes (plain JS) | Partial (UI is React-only) | | Custom integration logic | Full (code) | Mapping, remote data, passthrough | Workflows + custom connectors | Code-native TS | Proxy only | Actions, workflows, proxy | | Custom fields and objects | Full, in code | Plan-gated, limited | Via workflows | Via code | Field Mapping (Scale+); CRM custom objects (connector-dependent) | Via raw API | | Data syncs | Native, incremental | Managed (daily on entry plan) | Managed Sync (1-min default) | Flows + large data syncs | None (real-time) | Triggers + workflows | | Pricing | Public, free tier | Entry public, then sales-led | Sales-led | Sales-led | Public | Public | | Self-host or own-cloud | Self-host or BYOC (Enterprise; limited free) | Available for purchase (Enterprise) | Yes (Enterprise) | Own AWS account (Enterprise) | No | No | | Open source | Core platform (ELv2) | Proprietary | Proprietary | Connectors public | Proprietary | Components public | The catalog totals come from each vendor's site in August 2026 and are not directly comparable, because vendors count APIs, connectors, and components differently. "Angular-friendly SDK" means an official framework-agnostic path you can call from Angular, whether the package ships as TypeScript or plain JavaScript. "Open source" identifies which part of the stack is public, not merely whether a client SDK is available. "Custom integration logic" distinguishes code you control from provider-specific passthrough or proxy calls. ## FAQ ### How do I add email sync to my SaaS product without building IMAP infrastructure? Use an embedded integration platform that manages Gmail and Microsoft Graph for you; those two APIs cover Google Workspace and Microsoft 365 mailboxes with no IMAP involved. Most of the work lies in OAuth token refresh, Gmail's Pub/Sub `watch` renewals, Graph subscription lifecycles, and sync state rather than the API calls themselves. Nango, for example, handles auth and runs the sync as code you control. ### What are the best embedded integration platforms for B2B SaaS products enabling customer self-service connector setup? All six platforms here support self-service setup, where end users link their own accounts from your UI. What varies is how much of the interface you can brand and how much configuration customers can manage themselves. Nango's connect UI and Paragon's Connect Portal both offer headless modes, and Prismatic embeds a whole self-service marketplace. Merge's white-label and Link customization features sit on its Enterprise plan, and Apideck requires its Enterprise plan to remove Vault branding. If customers must also configure field mappings or other integration settings, check whether they do that through your code, a visual builder, or the platform's UI. ### Which embedded integration platforms have a visual flow designer and pre-built connectors? Paragon, Prismatic, and Pipedream pair pre-built connectors with a visual flow designer; Prismatic also lets you embed the designer for end customers. Before choosing one, confirm that its step, runtime, and concurrency limits can handle your syncs and provider-specific logic. ### What are the highest rated embedded integration platforms for connecting customer tools? There is no stable independent rating for this category. Based on the criteria above, Nango fits engineering teams building deep, custom integrations. Merge fits teams that need unified APIs with fixed data models, while Paragon fits non-technical teams that want low-code workflows. All six platforms let customers connect their own tools from inside your product. ## Conclusion Integration requirements rarely stay simple. One customer may depend on dozens of custom Salesforce fields, while another needs an API the platform does not cover. Once requests like these arrive, catalog depth and control over the integration logic matter more than how quickly the first demo came together. For teams building deep, custom integrations, Nango is the strongest option in this comparison. Its catalog spans more than 900 APIs, the integration logic remains in code, and teams can choose Nango Cloud, self-hosting, or BYOC on an Enterprise plan. Merge is a better fit for teams that need unified APIs with fixed data models rather than custom object access, while Paragon and Prismatic suit teams that prefer a visual approach. Whatever platform you choose, keep provider credentials out of the browser. The frontend should receive only a short-lived token, while the code that reads and transforms customer data stays under your team’s control. ### Code Became Cheap. Did Developer Knowledge Too? URL: https://www.angularspace.com/code-became-cheap-did-developer-knowledge-too/ Last updated: 2026-08-24T15:52:12.000Z --- *When implementation becomes abundant, selection becomes the work.* For roughly eight years, I had a simple model of becoming a better developer. Read the documentation. Learn Angular properly. Understand RxJS, dependency injection, change detection, testing, state management, architecture. Look at how other people solve problems. Make enough mistakes to understand why one solution is better than another. The model was not perfect, but it was easy to follow: the more I understood, the more I could do on my own, and the faster I could find a good solution, the better developer I became. Then AI arrived and made a large part of what I had trained for surprisingly cheap. It can write the implementation in minutes. It can find a library I have never used, read its documentation, compare several approaches and produce a working starting point before I finish browsing the first API page. This is not another article about AI taking developers’ jobs. I do not know how that story ends. I am interested in a more immediate and more uncomfortable question. If code became cheap, what happened to the value of everything I learned? ### **What Actually Became Cheap** I no longer need to remember an exact API. I do not need every RxJS operator ready in my head. I can ask an agent to inspect a package, search the documentation and begin an implementation. Pretending this changes nothing would be dishonest. A part of my knowledge has lost market value: remembering syntax and reproducing familiar implementations is less scarce than it used to be. But scarcity is not the same as usefulness. AI may not have made my knowledge worthless. It may have removed my monopoly on applying it. That distinction matters because generating an answer and deciding whether it belongs in a real system are different activities. The first is getting dramatically cheaper. The second still depends on context: the product, the codebase, the team, the cost of failure and what is likely to change next. The evidence on productivity is less tidy than the demos suggest. In a 2025 randomized study, METR found that experienced open-source maintainers working in repositories they knew took 19% longer with early-2025 AI tools. The researchers carefully limited that conclusion to the tools, developers and tasks they studied. Later METR work with newer agents reported small productivity benefits instead. The direction can change with the model and the task. [**METR’s 2025 study**](https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/?ref=angularspace.com) is not proof that AI slows developers in general, just as a two-hour app demo is not proof that production software has become trivial. ![Relative task time in METR's study: 100 without generative AI and 119 with AI allowed.](https://cdn-images-1.medium.com/max/800/1*EYOItBlKQSvX_ZtJ39JQmA.png) Relative task time in METR’s study: 100 without generative AI and 119 with AI allowed. *In this setting, access to early-2025 AI tools increased completion time by 19%. The study covered 16 experienced open-source developers completing 246 issues in repositories they knew well.* Maybe “Does AI make us faster?” is already too broad a question. Faster at what? --- ### **A Framework Is Not a Domain** There was a related trap in our industry long before generative AI. When I entered software development, it was easy to confuse a domain with a technology. Frontend meant Angular or React. Backend meant Java, .NET or Node. We described ourselves as Angular developers, React developers and Java developers. I am an Angular developer. For years, that specialization gave me a useful direction. It also made it tempting to learn tools before problems. We need state? Add a library. Validation? A library. Caching? A library. Communication between components? Surely there is another library. Over time, I tried to reverse the order. What problem do we actually have? Is it a known class of problem? Are we dealing with an observer, a queue, a cache, a state machine, a concurrency policy or an ownership boundary? Only then: which Angular feature or library fits it? This order is even more important now. AI is very good at answering, “How do I implement this in Angular?” The harder question is, “Should we implement it this way at all?” ### **What Angular Taught Me Beyond Angular** If a model knows more Angular APIs than I do, does Angular expertise still matter? I think it does, but not for the reason I once assumed. RxJS taught me to think about time, streams and concurrency. Consider **switchMap**, **concatMap**, **mergeMap** and **exhaustMap**. An agent can list their behavior and write code with any of them. The choice is still a product decision expressed through code. Should a new request cancel the previous one? Wait behind it? Run at the same time? Be ignored while work is in progress? There is no correct operator until we define the behavior we want. Dependency injection taught me to see dependencies and inversion of control. Angular’s own documentation defines DI as supplying a class with dependencies instead of making it create them internally. The API is Angular-specific. The question of who creates, owns and can replace a dependency is not. [Angular’s DI guide](https://angular.dev/guide/di?ref=angularspace.com). State-management libraries forced me to ask where state should live, who can change it and how far it should travel. Components taught me about responsibility boundaries. The router taught me that navigation is also state. Tests taught me to define what I expect before trusting an implementation. Even signals are more than a function call. Angular describes them as a system that tracks where state is used so updates can be propagated efficiently. Knowing the API helps. Understanding dependencies, derived state and reactive context helps when the generated implementation becomes surprising. [Angular’s Signals guide](https://angular.dev/guide/signals?ref=angularspace.com). Maybe the real value of eight years with a framework is not the API I can recall. It is the collection of problems I learned to recognize through it. That collection makes AI more useful, not less. ### **The Wrong Question About Verification** As AI writes more code, two strong positions keep appearing. At one end is outcome-first development. Describe the task, let the agent implement it, run the application, check the result and accept the change. Do not spend time reading every line if the behavior is correct. At the other end is code-first craftsmanship. AI may type the implementation, but a developer should still understand the structure, inspect the important details and leave the codebase maintainable. The material that inspired this article described three versions of this spectrum: judging the outcome, defining the contract and constraints, or caring closely about the generated code. I do not think one of them wins everywhere. The better question is not, “Should I read AI-generated code?” It is, “How will I build enough trust in this change?” For a script that renames a few local files once, I may inspect the inputs, run it on copies and compare the output. Reading every line may add little. For a prototype that will be discarded next week, a smoke test may be a rational level of evidence. For authorization, payments, user data or a foundation that the team will extend for five years, “it seems to work” is nowhere near enough. The code is cheap in all three cases. Failure is not. ### **Verification Is Part of Programming** Verification does not mean only reading code. It can include: - a narrow change with explicit acceptance criteria - type checking, linting and static analysis - unit, integration and end-to-end tests - security checks and dependency review - a second human or agent challenging the solution - production observability - staged rollout and a reliable way to revert None of these is magical. Tests can confirm the wrong requirements. A reviewer can miss a subtle error. Types cannot prove that a business rule is correct. Observability can tell us about damage only after deployment. Trust comes from combining evidence appropriate to the risk. I find it useful to look at five properties of a change: ![Three verification levels showing stronger evidence as software risk rises from a disposable script to authorization, payments and user data.](https://cdn-images-1.medium.com/max/800/1*-ybZO7aH6-dMyQauTG9Dfw.png) Three verification levels showing stronger evidence as software risk rises from a disposable script to authorization, payments and user data. *Verification should scale with the cost and reversibility of failure.* --- Targeted is important. Experience does not always mean reading everything. Sometimes it means knowing where to look. This is close to what current developer surveys show. In Stack Overflow’s 2025 survey, more respondents distrusted the accuracy of AI tools than trusted it, and the most common frustration was an answer that was almost right. Developers are already discovering that generation and acceptance are separate tasks. [Stack Overflow Developer Survey 2025](https://survey.stackoverflow.co/2025/ai?ref=angularspace.com) DORA’s 2025 research describes AI as an amplifier of the engineering system around it. Faster generation helps most when teams also have fast, high-quality feedback loops. If a team already struggles to review, test and deploy small changes, producing more code can magnify the weakness. [DORA 2025](https://dora.dev/research/2025/dora-report/?ref=angularspace.com). The bottleneck may be moving from production to evaluation. But that does not make the evaluator passive. Designing the evidence is now part of the implementation. ### **Experience as the Ability to Choose** An experienced developer has usually seen abstractions that promised simplicity and made everything harder six months later. - Global state introduced too early. - A library that became impossible to remove. - A framework applied to a problem that needed a small function. - A locally elegant design that did not fit the rest of the system. - This is difficult to encode as API knowledge. It is a library of consequences. AI can give me five architecturally respectable solutions. My experience may help me notice that one is too unfamiliar for the team, one creates a permanent dependency for a temporary problem, and one assumes a scale we will probably never reach. That does not mean the senior developer should defend familiar solutions against anything new. AI can search a wider space than I can. It can expose me to a pattern I did not know and challenge an assumption I stopped noticing. The value is not in always being right. It is in asking what the alternatives optimize for, which constraints matter here and what evidence could prove the choice wrong. Experience is becoming less about owning the implementation and more about taking responsibility for the selection. ### **Should We Still Learn Programming From the Ground Up?** This question becomes harder for people entering the field. If an agent can generate an application, should a junior still learn the fundamentals? I think so, but the reason changes. The goal is not to reproduce every implementation from memory. It is to build a model of what the system is doing. What happens when a request starts? Where does state live? What happens when the same event arrives twice? What does cancellation mean? Why does the view render again? What does a cache return after the source changes? Where does an error go? Without that model, a working result is hard to distinguish from a result that works only in the happy path. There is a real paradox here. AI can make programming easier to learn because it can explain unfamiliar code, generate examples and provide immediate feedback. It can also make it easy to skip the exact friction that builds a useful mental model. We do not yet have strong long-term evidence about what constant agent use will do to junior development. So I do not want to turn this concern into a fact. But I would not confuse completing more exercises with understanding more systems. ### **Adaptation or FOMO?** Developers have always had fear of missing out. Learn React. Angular is dying. Try Vue. Adopt the new state library. Rewrite everything with the latest pattern. AI multiplied the pressure: a new model, agent, editor, MCP server, skill, subagent, context technique or software factory every week. Someone deleted their IDE. Someone else no longer reads code. A third person built an application in two hours. It is easy to conclude that the way I work is already obsolete. I try to separate adaptation from FOMO with a simple test. Adaptation starts with a problem and looks for a tool that improves the outcome. FOMO starts with a tool and looks for a problem that justifies using it. This is the same mistake we made with libraries, only at a higher speed. Yesterday we wanted a library for every problem. Today we may want an agent for every problem. The vocabulary changed. The thinking did not. I do need to update my workflow. AI is not just another autocomplete, and some skills I spent years practicing will matter less. Ignoring that would be nostalgia. But adaptation does not require copying the workflow of somebody building disposable side projects when I am changing a ten-year-old system. Context is not resistance to change. It is the reason engineering exists. ### **The Cost of a Good Decision** So what is valuable when code is cheap? Recognizing the problem. Defining constraints. Choosing tools. Understanding trade-offs. Specifying what correct means. Building evidence. Debugging what the agent did not anticipate. Understanding the product. Simplifying. Knowing the cost of failure. Deciding that something does not need to be built. And accepting responsibility for the result, even when I did not type it. AI has lowered the cost of producing code. It has not necessarily lowered the cost of a good decision. Looking back at eight years of learning Angular and software development, I no longer think the time was wasted. I may simply have described the outcome incorrectly. I thought I was learning to write increasingly better code. Much of the time, I was learning to recognize problems, predict consequences and remember solutions that once looked clever and later failed. Today I am learning when to let AI write the code for me, when to read it, how to verify it and when the best decision is not to generate it at all. Maybe that is still learning to program. Programming just no longer means only writing code. ### How to Handle Global Errors in Modern Zoneless Angular URL: https://www.angularspace.com/how-to-handle-global-errors-in-modern-zoneless-angular/ Last updated: 2026-08-04T11:51:47.000Z In the world of web development, unhandled errors are silent threats. They can degrade user experience, cause unpredictable application behavior, and leave developers in the dark. For years, Angular's error handling has benefited from a bit of "magic" provided by `Zone.js`. However, as the framework evolves towards a more explicit and performant zoneless future, that magic disappears, requiring a new approach. This is the primary reason [provideBrowserGlobalErrorListeners](https://angular.dev/api/core/provideBrowserGlobalErrorListeners?ref=angularspace.com) was introduced. It’s not just a new feature of the Angular v20, but a fundamental tool for robust error handling in modern, zoneless Angular. But lets start from the beginning. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/08/feature-image-1.png) ### Error Handling with Zone.js: The Old Magic In a traditional, "Zone-full" Angular application, `Zone.js` acts like an invisible net cast over our application's execution context. It works by "monkey-patching" nearly all standard asynchronous browser APIs, such as `setTimeout`, `addEventListener`, and, critically, **`Promise`**. Because `Zone.js` wraps these APIs, it gains awareness of almost every async task that begins and ends. This mechanism is primarily used to trigger Angular's change detection automatically. However, it provides a powerful side effect for error handling: - **Automatic Interception:** When a `Promise` is rejected without a `.catch()` handler, `Zone.js`'s patched version of `Promise` is aware of this failure. It automatically intercepts the unhandled rejection and forwards it into Angular's execution context. - **Centralized Errors:** The result is that these "outside" errors are seamlessly channeled to Angular's central [ErrorHandler](https://github.com/angular/angular/blob/v22.1.0/packages/core/src/error%5Fhandler.ts?ref=angularspace.com#L54), where they can be processed just like an error from a component or service. The `ErrorHandler` is a simple, injectable class that serves as a centralized hook for all exceptions caught within the Angular framework. Its [default implementation](https://github.com/angular/angular/blob/v22.1.0/packages/core/src/error%5Fhandler.ts?ref=angularspace.com#L60-L62) is straightforward. You can read more about it in the [Armens's article about Angular Error Handling](https://www.angularspace.com/angular-error-handling/) This was convenient, but it relied on the implicit, pervasive nature of `Zone.js`. ### The Zoneless Challenge: When the Magic Disappears A zoneless application, by definition, does **not** include `Zone.js`. This brings significant performance gains and makes application behavior more explicit, but it removes the invisible error-catching net. So what happens when an error occurs outside of Angular's direct control in a zoneless application? Consider these common scenarios: - **Unhandled Promise Rejections:** A `Promise` is rejected somewhere in our code, but we forgot to chain a `.catch()` handler. - **Third-Party Scripts:** An error occurs inside a non-Angular, third-party library that manipulates the DOM or performs its own asynchronous tasks. - **Asynchronous Operations:** A callback passed to a native browser API like `setTimeout` or an event listener added with `addEventListener` throws an error. Without a specific mechanism in place, these errors would not be intercepted by Angular's `ErrorHandler`. They would be logged to the console but would exist outside our centralized error-handling logic, making them invisible to our logging and reporting services. ### The Solution: Building a Bridge with`provideBrowserGlobalErrorListeners` This is precisely the problem `provideBrowserGlobalErrorListeners` solves. It serves as the explicit replacement for the implicit magic that Zone.js once provided for error handling. Instead of patching anything, this provider sets up simple, native event listeners on the global window object for: 1. [error](https://github.com/angular/angular/blob/v22.1.0/packages/core/src/error%5Fhandler.ts?ref=angularspace.com#L125): Fired for general runtime script errors. 2. [unhandledrejection](https://github.com/angular/angular/blob/v22.1.0/packages/core/src/error%5Fhandler.ts?ref=angularspace.com#L121): Fired whenever a `Promise` is rejected without a handler to catch the rejection. When one of these global events is triggered, the listener created by `provideBrowserGlobalErrorListeners` intercepts the raw error and \*\*forwards it to Angular's centralized `ErrorHandler` class. This re-establishes the connection that was lost when Zone.js and creates a unified pipeline, ensuring that errors from third-party scripts, native browser APIs, or unhandled promises are treated the same as errors from within our components or services. This allows us to process every error in one central location, whether for logging, analytics, or displaying user-friendly notifications. This function is a default part of the Angular's setup. When we create a new project using the Angular CLI, the `provideBrowserGlobalErrorListeners()` provider is automatically added to the `app.config.ts` file, ensuring that robust error handling is built-in from the very beginning. `export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), // ... ] };` ### Implications for Testing This improved error capturing has an important consequence for testing. Errors thrown in event listeners are now reported to the internal Angular error handler. This is a positive change for application quality, as it means we may see errors in our tests that were not reported before, uncovering previously hidden bugs. The recommended approach is to fix the underlying issues causing these errors in our tests. However, if that's not immediately feasible, we have an escape hatch. We can configure the `TestBed` to prevent these errors from failing our test suite by setting `**rethrowApplicationErrors: false**` in the `configureTestingModule` setup as a last resort: `TestBed.configureTestingModule({ // ... other testing module configuration rethrowApplicationErrors: false, // Use only when necessary });` ### Customization and Best Practices The Angular team **recommends handling these global errors for most applications**, and `provideBrowserGlobalErrorListeners` is the easiest way to do so. It offers a "plug-and-play" solution that integrates seamlessly with the framework. However, Angular remains flexible. If our application has specific needs that require a different approach, we are free to implement our own custom listeners for the `window.error` and `window.unhandledrejection` events. If we choose to provide our own custom listeners, we can, and should **remove `provideBrowserGlobalErrorListeners`** from our `app.config.ts` providers to avoid setting up redundant handlers. ### Conclusion `provideBrowserGlobalErrorListeners` is a valuable and effective utility for Angular developers. It offers a consistent way to catch errors that occur outside the Angular context, seamlessly integrating them into Angular’s error handling flow. As Angular continues to evolve, the importance of explicitly managing and handling errors becomes even more important. By leveraging `provideBrowserGlobalErrorListeners` and custom `ErrorHandler` implementations, we can create Angular applications that are more robust, stable, and user-focused. ### The Strange Theatre of Technical Hiring URL: https://www.angularspace.com/the-strange-theatre-of-technical-hiring/ Last updated: 2026-06-18T18:15:04.000Z # A senior engineer joins a technical interview... Hi Angular Space community :) It’s been a while since I pushed myself to write something. AI is changing the way we consume literally everything online, and I’m still trying to figure out what the best and most compelling way forward looks like for sharing Angular knowledge through Angular Space. For now, I’m touching on some universal problems that many of us have either already faced or are facing right now. Enjoy & share with your HR/Hiring Managers and technical recruiting team 😄 --- # A senior engineer joins a technical interview... They have years of experience behind them. They have shipped systems, reviewed architecture, mentored developers, debugged production issues, worked with messy codebases, and made decisions that affected real clients and real users. Then the interview begins: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/06/ChatGPT-Image-Jun-17--2026--06_21_04-AM.png) \\ Just a problem, a timer, and someone watching. For the next hour, the job disappears. What remains is a performance. That is the strange theatre of technical hiring. Software engineering today is collaborative, contextual, tool-assisted, and deeply dependent on judgment. But many interviews still test developers as if the job happens alone, from memory, under observation, with most of the important tools removed. The company says it wants engineering ability, but the interview often rewards something narrower: speed, recall, confidence under pressure, and familiarity with interview rituals. That does not mean those skills have no value. It means they are not the whole job. And for senior engineers, leads, and architects, they may not even be the most important part of the job. ## The stories are everywhere Recently, I saw several developers describe almost the same experience.One staff-level engineer wrote about a live coding round that left him feeling humiliated despite nearly twenty years in the industry. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/06/IMG_7835.jpg) Keith was not someone new to software. He was someone who had worked on distributed systems, fintech platforms, microservices, and AI workflows. In real work, that kind of experience matters. In the interview, it seemed to matter less than whether he could solve a narrow puzzle under pressure. Another developer described a similar pattern. The conversations with engineering leadership went well. He could explain his experience, his thinking, and his approach to building software.Then the coding challenge arrived and the rejection followed. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/06/IMG_8503.jpg) Jessies point was simple: coding challenges are often tests with a compiler. They may show how someone performs in a test environment, but not necessarily how they learn, adapt, collaborate, or build inside a real team. Another engineer added an important perspective: some people are not fast live performers, but they are excellent deep thinkers. In real work, where they can process information, use resources, ask questions, and verify decisions, they perform well. In timed interviews, they look weaker than they are. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/06/IMG_8504-1.jpg) Because software engineering is not a speed-reading contest. It is not a memory contest. It is not performance art. At least, it should not be... ## The interview often removes the actual job A senior developer’s real work rarely begins with an empty file. It begins with context. There is an existing system. There are old decisions. There are constraints. There are tests that may or may not be trustworthy. There is documentation that may or may not be current. There are teammates who know why something was built in a strange way. Real engineering is not only writing code. It is: - understanding why the code exists. - knowing what not to change. - recognizing when a simple solution is better than a clever one. It is also asking mamy questions such as: - What problem are we actually solving? - What could this break? - What is the cost of this abstraction? - Will the team understand this in six months? - Does this belong in the codebase at all? These are senior engineering questions. But many interviews do not create space for them. Instead, the candidate is placed in an artificial environment and judged on how well they perform there. - A developer can be excellent at the job and still look average in the interview. - A developer can be excellent at the interview and still struggle with the job. That should worry companies more than it does. ## The Angular architect problem ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/06/ChatGPT-Image-Jun-17--2026--07_45_43-AM.png) This mismatch becomes even more dangerous when the role is senior or architectural. Another real life story is with a company hiring for an Angular architect. However instead of checking the skills that they are hiring for they decided to go with a generic JavaScript route openly saying there is no additional Angular related step to the process. The client does not need someone who merely knows Angular syntax. The client needs someone who can make decisions that will shape the project for months or years. An Angular architect may influence: - How the Angular workspace is structured (single app vs multi-project / monorepo) - How applications and libraries are split, and which features are packaged as reusable Angular libraries. - How teams share code through internal libraries and dependency injection patterns. - How state is managed (signals, RxJS, forms, server state) and where that state lives. - How domain boundaries are protected inside a multi-project workspace or monorepo. That is the value of an Angular architect. The value is not only in knowing JavaScript trivia. It is not only in answering TypeScript quiz questions. It is not only in solving algorithm puzzles. And yet this is often what happens. A company says it needs someone to protect the architecture of a client’s Angular projects, but then barely tests Angular architectural thinking. That is not just unfair to the candidate. It is risky for the client. Angular project will not fail because the architect forgot a JavaScript quiz answer. It will fail because: - the Business side of things in misunderstood and under-investigated - the wrong Angular boundaries were chosen (apps vs libraries, workspace layout). - complexity was introduced too early through unnecessary libraries or cross-cutting dependencies. - teams were not aligned on Angular patterns and shared libraries. - nobody challenged the Angular architecture before it became expensive Interview tested the wrong skill and still felt confident in the result. ## The AI contradiction ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/06/ChatGPT-Image-Jun-17--2026--07_55_51-AM.png) AI has made this contradiction more visible. The industry now tells developers to become AI-augmented. Use AI assistants. Use documentation. Use search. Use autocomplete. Use tests. Use code review. Use tools to move faster and reduce friction. In real work, the modern engineer is expected to use everything available to produce better software. But in interviews, the message often becomes the opposite. - Do not use AI. - Do not use Google. - Do not use documentation. - Do not use the environment you normally use to work safely. - Solve this from memory. This creates a strange contradiction. At work, the senior engineer is expected to use tools responsibly. In the interview, they may be judged after those tools are removed. The point is not that AI should be allowed to do the thinking for the candidate. That would be the wrong lesson. The real point is that modern engineering skill now includes knowing how to work with AI safely. - Can the candidate verify AI output? - Can they spot hallucinations? - Can they write tests around generated code? - Can they reject a plausible but wrong answer? - Can they explain why a suggestion does not fit the architecture? - Can they use AI as a tool without outsourcing their judgment? That is a much better senior-engineer assessment than asking someone to remember syntax under pressure. The question should not be: - Can you solve this alone, from memory, without tools? The better question is: - Can you use tools, context, and judgment to produce reliable software? ## Software engineering hiring appears stuck ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/06/ChatGPT-Image-Jun-17--2026--07_28_43-AM.png) Many other technical professions have long used more contextual and applied ways to assess experienced professionals. **Civil and mechanical engineers** are often evaluated through portfolios of real projects, discussions of existing designs, review of calculations, and decisions made under realistic constraints: safety codes, budgets, site conditions, regulations, and long-term maintainability. **Physicians advance through supervised clinical practice**, residency, practical examinations with simulated patients, and certifications that test applied clinical judgment rather than isolated theoretical recall. **Lawyers are often assessed through writing samples**, moot court exercises, clerkships, case analysis, and other formats that resemble the actual work of legal reasoning and advocacy. These fields are not easy. Their assessments can be demanding. But they tend to respect an important truth: - Senior competence is best evaluated in context. Software engineering hiring, by contrast, often remains anchored in formats that became popular in the late 2000s and 2010s, however the work has changed, the job has evolved... ...too many interviews have not. ## Some companies are already looking for better signal ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/06/e39ace06-2300-4ffd-80ae-99d417e5c2aa-cover.png) This is not theory. Some companies already understand that interviews can reward performance more than real ability. Linear is a strong example. Their hiring process includes paid work trials as a final stage. Candidates work on real or close-to-real projects, with access to relevant tools, context, and people. The goal is not just to hear how someone talks about work - it is to see how they actually work. How they make decisions, communicate, handle ambiguity, use feedback, take ownership, and collaborate with the team. This approach has produced excellent results. Over several years, Linear has hired dozens of people through work trials with a[reported 96% retention rate among those hires. ](https://linear.app/now/why-and-how-we-do-work-trials-at-linear?ref=angularspace.com) Research in personnel psychology supports this direction. [Meta-analyses, including the foundational work by Schmidt and Hunter](https://www.plum.io/blog/schmidt-hunter-meta-analysis?ref=angularspace.com), have consistently found that **work sample tests** \- assessments that simulate actual job tasks - show the highest predictive validity for future job performance among common selection methods. They outperform many traditional unstructured or puzzle-based interviews in forecasting real-world success. ## The problem is not difficulty When developers criticize coding interviews, some people hear they want the process to be easier. But that is not the real argument. A realistic interview can be harder than a puzzle. - Debugging an unfamiliar codebase is hard. - Reviewing architecture is hard. - Explaining trade-offs is hard. - Working with unclear requirements is hard. - Using AI without being fooled by it is hard. - Making maintainable decisions under business pressure is hard. The issue is not that interviews are hard. The issue is that many interviews are hard in the wrong way. - They create pressure, but not the same pressure as the job. - They test speed, but not always judgment. - They test recall, but not always reasoning. - They test isolated problem-solving, but not always collaboration. - They test whether someone can perform engineering theatre, not whether they can do engineering work. ## What should we test instead? If the role requires architecture, test architecture. If the role requires leadership, test communication and judgment. If the role requires debugging, test debugging. If the role requires working with legacy systems, give the candidate legacy constraints. If the role requires AI-assisted development, test whether they can use AI responsibly. When pair programming or live collaboration sessions are used, they should happen in the actual development environment the team uses in real work - with full access to the IDE, internal tools, documentation, version control, testing frameworks, CI pipelines, and AI coding assistants - not in artificial, stripped-down environments that remove the very supports modern engineers rely on daily. Only then does the exercise meaningfully reflect collaborative engineering. The best hiring process is not the one with the hardest questions. It is the one with the most relevant signal. ## The job we still imagine There is an old image of programming that still lives inside many technical interviews. One developer. One problem. One correct answer. But it is not how most serious software gets built. Real software engineering is collaborative, contextual, tool-assisted, and full of trade-offs. It requires memory, yes, but also judgment. It requires coding, yes, but also communication. It requires technical knowledge, yes, but also the ability to use that knowledge inside messy systems with real constraints. That is why these stories resonate. - The staff engineer who felt humiliated after nearly twenty years. - The developer who could succeed in real jobs but not in coding challenges. - The engineer who thinks deeply but not quickly under artificial pressure. - The Angular architect evaluated on everything except the architecture the client actually needed. These are not isolated complaints, they are symptoms of the same problem. Software engineering has changed, the tools have changed, the expectations have changed, the risk has changed. Now technical hiring has to change too. The goal should not be to hire the person who performs best in the theatre of the interview. **The goal should be to hire the person who can do the job.** ### Angular Space is picking up new wind! URL: https://www.angularspace.com/angular-space-is-picking-up-new-wind/ Last updated: 2026-04-22T16:26:00.000Z We’re excited to welcome 👉 [CodeRabbit](https://www.linkedin.com/company/coderabbitai/?ref=angularspace.com) 👈 as our NEW sponsor and continue growing the community together 🥳🎉 I have been using CodeRabbit for AI reviews and I Am honestly very impressed how useful it is with accurate hints Perfect match for Angular Space! Check it out: [https://coderabbit.link/danielAS](https://coderabbit.link/danielAS?ref=angularspace.com) ### Angular v20 Custom MatPaginator Styling URL: https://www.angularspace.com/angular-v20-custom-matpaginator-styling/ Last updated: 2026-01-30T14:48:55.000Z One of my more popular articles that I’ve published is [Angular: MatPaginator Custom Styling](https://dev.to/krivanek06/angular-matpaginator-custom-styling-dkb?utm%5Fsource=chatgpt.com), wrapping up around 17K views, which shows how to transform Angular Material’s paginator (on a mat-table) using a custom directive to make it look more appealing. I decided to update this article because since then, two major things have changed. It was originally written for Angular v14, and since then we’ve received the major [Angular Material MDC components](https://v17.material.angular.dev/guide/mdc-migration?ref=angularspace.com), which caused quite a few issues for people updating their projects, along with some smaller Angular improvements. Also, in my previous article, I was accessing some private methods of the `MatPaginator` component. In the GIF below, you can see the final example we’ll be building. You can also find the full source code in this [GitHub repository](https://github.com/krivanek06/stackblitz-angular-custom-mat-paginator?ref=angularspace.com). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/11/angular-custom-mat-pagination-example.gif) Angular Custom MatPaginator End Result I will start this blog post by showing the full code section, you can always come back to it, and then we’ll go through some of the more interesting or not that obvious parts. You might already have a table component displaying data, something like this: ```typescript import { afterNextRender, Component, viewChild } from '@angular/core'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; type Data = { position: number; name: string; weight: number; symbol: string }; @Component({ selector: 'app-test-table', imports: [MatTableModule, MatPaginatorModule, BubblePaginationDirective], template: `
No. {{ element.position }}
`, }) export class TestTableComponent { readonly dataSource = new MatTableDataSource([]); readonly paginator = viewChild(MatPaginator); readonly displayedColumns: string[] = ['position', 'name', 'weight', 'symbol']; constructor() { const data: Data[] = []; Array.from({ length: 100 }, (_, k) => k + 1).forEach(v => { data.push({ position: v, name: `Element ${v}`, weight: v * 1.5, symbol: `E${v}` }); }); this.dataSource.data = data; afterNextRender(() => { const paginator = this.paginator(); if (paginator) { this.dataSource.paginator = paginator; } }); } } ``` With this simple configuration, your table looks as follows: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/11/angular-table-basic.png) Basic Table Basic Pagination What we want to achieve is creating a directive that we can attach to the `mat-paginator` and it will transform the look of our paginated table into the bubble example. The usage of the directive will be the following: ```html ``` Here is the full code of the directive and below I describe some of its sections. ```typescript import { Directive, ElementRef, Renderer2, afterRenderEffect, inject, input, untracked } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; /** * Works from angular-material version 15. since all classes got the new prefix 'mdc-' */ @Directive({ selector: '[appBubblePagination]', }) export class BubblePaginationDirective { private readonly matPag = inject(MatPaginator, { optional: true, self: true, host: true, }); private readonly elementRef = inject(ElementRef); private readonly ren = inject(Renderer2); /** * whether we want to display first/last button and dots */ readonly showFirstButton = input(true); readonly showLastButton = input(true); /** * total number of items in pagination * needed to calculate how many buttons to render * when page size changes */ readonly paginationSize = input(0, { alias: 'appBubblePagination', }); /** * how many buttons to display before and after * the selected button */ readonly renderButtonsNumber = input(2); /** * references to DOM elements */ private dotsEndRef!: HTMLElement; private dotsStartRef!: HTMLElement; private bubbleContainerRef!: HTMLElement; /** * ref to rendered buttons on UI that we can remove them size changes */ private buttonsRef: HTMLElement[] = []; readonly buildButtonsEffect = afterRenderEffect(() => { // rebuild buttons when pagination size change this.paginationSize(); untracked(() => { // remove buttons before creating new ones this.removeButtons(); // set some default styles to mat pagination this.styleDefaultPagination(); // create bubble container this.createBubbleDivRef(); // create all buttons this.buildButtons(); // switch back to page 0 this.switchPage(0); }); }); /** * change the active button style to the current one and display/hide additional buttons * based on the navigated index */ private changeActiveButtonStyles(previousIndex: number, newIndex: number) { const previouslyActive = this.buttonsRef[previousIndex]; const currentActive = this.buttonsRef[newIndex]; if (!previouslyActive && !currentActive) { return; } // remove active style from previously active button if (previouslyActive) { this.ren.removeClass(previouslyActive, 'g-bubble__active'); } // add active style to new active button this.ren.addClass(currentActive, 'g-bubble__active'); // hide all buttons this.buttonsRef.forEach(button => this.ren.setStyle(button, 'display', 'none')); // show N previous buttons and X next buttons const renderElements = this.renderButtonsNumber(); const endDots = newIndex < this.buttonsRef.length - renderElements - 1; const startDots = newIndex - renderElements > 0; const firstButton = this.buttonsRef[0]; const lastButton = this.buttonsRef[this.buttonsRef.length - 1]; // last bubble and dots if (this.showLastButton()) { this.ren.setStyle(this.dotsEndRef, 'display', endDots ? 'block' : 'none'); this.ren.setStyle(lastButton, 'display', endDots ? 'flex' : 'none'); } // first bubble and dots if (this.showFirstButton()) { this.ren.setStyle(this.dotsStartRef, 'display', startDots ? 'block' : 'none'); this.ren.setStyle(firstButton, 'display', startDots ? 'flex' : 'none'); } // resolve starting and ending index to show buttons const startingIndex = startDots ? newIndex - renderElements : 0; const endingIndex = endDots ? newIndex + renderElements : this.buttonsRef.length - 1; // display starting buttons for (let i = startingIndex; i <= endingIndex; i++) { const button = this.buttonsRef[i]; this.ren.setStyle(button, 'display', 'flex'); } } /** * Removes or change styling of some html elements */ private styleDefaultPagination() { const nativeElement = this.elementRef.nativeElement; const itemsPerPage = nativeElement.querySelector('.mat-mdc-paginator-page-size'); const howManyDisplayedEl = nativeElement.querySelector('.mat-mdc-paginator-range-label'); const previousButton = nativeElement.querySelector('button.mat-mdc-paginator-navigation-previous'); const nextButtonDefault = nativeElement.querySelector('button.mat-mdc-paginator-navigation-next'); // remove 'items per page' if (itemsPerPage) { this.ren.setStyle(itemsPerPage, 'display', 'none'); } // style text of how many elements are currently displayed if (howManyDisplayedEl) { this.ren.setStyle(howManyDisplayedEl, 'position', 'absolute'); this.ren.setStyle(howManyDisplayedEl, 'color', '#919191'); this.ren.setStyle(howManyDisplayedEl, 'font-size', '14px'); this.ren.setStyle(howManyDisplayedEl, 'left', '-0'); } // check whether to remove left & right default arrows this.ren.setStyle(previousButton, 'display', 'none'); this.ren.setStyle(nextButtonDefault, 'display', 'none'); } /** * creates `bubbleContainerRef` where all buttons will be rendered */ private createBubbleDivRef(): void { const actionContainer = this.elementRef.nativeElement.querySelector('div.mat-mdc-paginator-range-actions'); const nextButtonDefault = this.elementRef.nativeElement.querySelector('button.mat-mdc-paginator-navigation-next'); // create a HTML element where all bubbles will be rendered this.bubbleContainerRef = this.ren.createElement('div') as HTMLElement; this.ren.addClass(this.bubbleContainerRef, 'g-bubble-container'); // render element before the 'next button' is displayed this.ren.insertBefore(actionContainer, this.bubbleContainerRef, nextButtonDefault); } /** * helper function that builds all button and add dots * between the first button, the rest and the last button * * end result: (1) .... (4) (5) (6) ... (25) */ private buildButtons(): void { if (!this.matPag) { return; } const neededButtons = Math.ceil(this.matPag.length / this.matPag.pageSize); // if there is only one page, do not render buttons if (neededButtons === 0 || neededButtons === 1) { this.ren.setStyle(this.elementRef.nativeElement, 'display', 'none'); return; } // set back from hidden to block this.ren.setStyle(this.elementRef.nativeElement, 'display', 'block'); // create first button this.buttonsRef = [this.createButton(0)]; // add dots (....) to UI this.dotsStartRef = this.createDotsElement(); // create all buttons needed for navigation (except the first & last one) for (let index = 1; index < neededButtons - 1; index++) { this.buttonsRef = [...this.buttonsRef, this.createButton(index)]; } // add dots (....) to UI this.dotsEndRef = this.createDotsElement(); // create last button to UI after the dots (....) this.buttonsRef = [...this.buttonsRef, this.createButton(neededButtons - 1)]; } /** * Remove all buttons from DOM */ private removeButtons(): void { this.buttonsRef.forEach(button => { this.ren.removeChild(this.bubbleContainerRef, button); }); // remove dots if (this.dotsStartRef) { this.ren.removeChild(this.bubbleContainerRef, this.dotsStartRef); } if (this.dotsEndRef) { this.ren.removeChild(this.bubbleContainerRef, this.dotsEndRef); } // Empty state array this.buttonsRef.length = 0; } /** * create button HTML element */ private createButton(i: number): HTMLElement { const bubbleButton = this.ren.createElement('div'); const text = this.ren.createText(String(i + 1)); // add class & text this.ren.addClass(bubbleButton, 'g-bubble'); this.ren.setStyle(bubbleButton, 'margin-right', '8px'); this.ren.appendChild(bubbleButton, text); // react on click this.ren.listen(bubbleButton, 'click', () => { this.switchPage(i); }); // render on UI this.ren.appendChild(this.bubbleContainerRef, bubbleButton); // set style to hidden by default this.ren.setStyle(bubbleButton, 'display', 'none'); return bubbleButton; } /** * helper function to create dots (....) on DOM indicating that there are * many more bubbles until the last one */ private createDotsElement(): HTMLElement { const dotsEl = this.ren.createElement('span'); const dotsText = this.ren.createText('.....'); // add class this.ren.setStyle(dotsEl, 'font-size', '18px'); this.ren.setStyle(dotsEl, 'margin-right', '8px'); this.ren.setStyle(dotsEl, 'padding-top', '6px'); this.ren.setStyle(dotsEl, 'color', '#919191'); // append text to element this.ren.appendChild(dotsEl, dotsText); // render dots to UI this.ren.appendChild(this.bubbleContainerRef, dotsEl); // set style none by default this.ren.setStyle(dotsEl, 'display', 'none'); return dotsEl; } /** * Helper function to switch page */ private switchPage(i: number): void { if (!this.matPag) { return; } const previousPageIndex = this.matPag.pageIndex; // switch page index of mat paginator this.matPag.pageIndex = i; // change active button styles this.changeActiveButtonStyles(previousPageIndex, this.matPag.pageIndex); // need to trigger page event manually, because we are changing pageIndex programmatically this.matPag.page.emit({ pageIndex: i, pageSize: this.matPag.pageSize, length: this.matPag.length, previousPageIndex: previousPageIndex, }); } } ``` ### Injecting Dependencies ```typescript inject(MatPaginator, { optional: true, self: true, host: true }) inject(ElementRef) inject(Renderer2) ``` - `MatPaginator` \- we hook into its current `pageIndex`, `pageSize`, and its `page` stream - `ElementRef` \- root element of the paginator (so we can query its internals) - `Renderer2` \- the safe way to create elements, set styles/classes, and listen to events. Works nicely with SSR and avoids direct DOM APIs ### Input / Output Bindings - `showFirstButton = input(true);` & `showLastButton = input(true);` \- whether to display first and last buttons for fast navigation on the start / end of the table - `renderButtonsNumber = input(2);` \- when you are on an active page, let’s say index `6` , then how many buttons before and after should we display - `paginationSize` \- this input is required to detect changes in the table length (after filtering or loading new data). When it changes, the directive re-renders the bubbles to match the correct number of pages. ### Core Logic Execution The directive itself is around 300 lines of code, but when you try to understand how it works, the main logic is inside the `buildButtonsEffect` effect, such as: ```typescript readonly buildButtonsEffect = afterRenderEffect(() => { // rebuild buttons when pagination size change this.paginationSize(); untracked(() => { // remove buttons before creating new ones this.removeButtons(); // set some default styles to mat pagination this.styleDefaultPagination(); // create bubble container this.createBubbleDivRef(); // create all buttons this.buildButtons(); // switch back to page 0 this.switchPage(0); }); }); ``` What is great about the effect, is that, it will re-execute when length (size) of the elements change, so for example if you have one table, but doing server-side data filtering, then each time new data arrives, bubbles will be recalculated due to `paginationSize` signal input. The brief overview is described below. - `paginationSize` \- listen on page size changes (on new data) to recalculate bubbles - `removeButtons()` – clear previous custom DOM (if re-running) - `styleDefaultPagination()` – hide certain default Material bits and position labels - `createBubbleDivRef()` – create a container where our bubbles will live - `buildButtons()` – create bubbles + dots based on total length and page size - `switchPage(0)` – start from page 0 to keep things predictable You may also ask, why we use `afterRenderEffect` instead of `effect` ? The reason is that `afterRenderEffect` is executed only in the browser, but `effect` also runs on the server side. It could lead to some errors if your app supports SSR. For a deeper explanation, check out my [afterRenderEffect, afterNextRender, afterEveryRender & Renderer2 article](https://www.angularspace.com/afterrendereffect-afternextrender-aftereveryrender-renderer2/). ### Core Logic Execution - Switching Page Manually The custom bubbles live outside Angular Material’s built-in controls. That means when a user clicks a bubble, nothing in `MatPaginator` fires by itself, we have to connect the click to the paginator so the table (and anyone subscribed to `matPag.page`) reacts, hence the need for `switchPage()` function. ```typescript private createButton(index: number): HTMLElement { // our unique bubble showing a specific page - 1, 2, etc. const bubbleButton = this.ren.createElement('div'); // ... some code ... this.ren.listen(bubbleButton, 'click', () => { this.switchPage(index); }); // ... some code ... } private switchPage(index: number): void { if (!this.matPag) { return; } const previousPageIndex = this.matPag.pageIndex; // switch page index of mat paginator this.matPag.pageIndex = index; // change active button styles this.changeActiveButtonStyles(previousPageIndex, this.matPag.pageIndex); // trigger page event manually, we are changing pageIndex programmatically this.matPag.page.emit({ pageIndex: index, pageSize: this.matPag.pageSize, length: this.matPag.length, previousPageIndex: previousPageIndex, }); } ``` The key part of is `this.matPag.pageIndex = index;`, keeping the paginator’s internal state in sync with witch bubble was clicked. If you were to remove this line, the pagination would stop working. Next, since we are programmatically changing the index of the paginator (mentioned above), when you navigate though the items in the table, the `this.matPag.page` will not emit by itself, therefore we also need to manually emit this data. ### Paginator Style Updates Inside `styleDefaultPagination`, you can see we’re directly accessing the paginator’s internal HTML elements to restyle them. Is this ideal? Probably not. Since it relies on exact class selectors like `button.mat-mdc-paginator-navigation-next`, it’s fragile. These internal selectors can change between Material versions. This already happened when Material v15 introduced the new MDC components, which broke custom styling for many projects using `::ng-deep`. What we’re doing here is a similar kind of hack, but for our use case it’s the most practical solution available. Still, it’s worth being aware of the tradeoff. ### Custom Styles Once the logic is done, we still need some styling to make it actually look like bubbles. This part is mostly CSS (or SCSS), and you can adjust it to fit your own theme. In my example, each bubble is a simple flex container with centered text, a hover state, and an active state to highlight the current page. ```scss /* Custom paginator styles */ .g-bubble-container { display: flex; gap: 4px; } .g-bubble { background-color: #f0f0f0; border-radius: 50%; width: 34px; height: 34px; display: flex; align-items: center; justify-content: center; color: #2e2e2e; font-size: 14px; cursor: pointer; transition: 0.3s; &:hover { background-color: #636363; color: orange; } } .g-bubble__active { background-color: #636363; color: orange; } mat-paginator { background: transparent !important; /* need mat-paginator range to align with other mat-table elements */ position: relative; } /* override alignment for the labels that shows "x of y" */ .mat-mdc-paginator-range-label { margin: 0 !important; } ``` ### Things to Keep in Mind There are a few small details that are good to keep in mind: - Renderer2 limitations - you can’t directly use pseudo-classes (`::before`, `::after`) from within the directive, keep your visual parts inside SCSS files - Changing Angular Material internals - as mentioned earlier, `mat-mdc-` selectors can change in future Material versions, it’s one of the risks of this directive - Accessibility (a11y) - since these are custom clickable divs, you might want to add `role="button"` and `tabindex="0"` attributes so users can navigate the bubbles with a keyboard. You can also listen for `keydown` events and simulate click behavior with the space or enter key - SSR / Hydration - if you’re running Angular SSR, the directive should still work fine, since `Renderer2` is SSR safe and we are using `afterRenderEffect` to render bubbles only on the client-side - Active Button State - currently when we load more data into the table and re-execute the logic of rendering bubbles, we go back to the first page, so no active state was implemented yet - Important input `[appBubblePaginationLength]` to bound the table length. Without it, the `afterRenderEffect` will not be notified that new data have arrived and buttons will not be rebuilt ### Summary By the end of this post, you should have a working, MDC compatible paginator. The goal here wasn’t to replace Angular Material, but to show how far you can go using directives + Renderer2, to enhance an existing Material component, without creating your own custom paginator from scratch. Of course there may be some room for improvement, it is still a simple directive, so if you have any suggestions, feel free to comment. Hope you liked this example, catch more of my articles on [dev.to](https://dev.to/krivanek06?ref=angularspace.com), connect with me on [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com) or check my [Personal Website](https://eduardkrivanek.com/?ref=angularspace.com). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/01/Generated-Image-January-29--2026---3_00PM.jpeg) ### Gemini and Angular, Part II: Creating Generative UIs URL: https://www.angularspace.com/gemini-and-angular-part-ii-creating-generative-uis/ Last updated: 2026-01-22T13:48:08.000Z Let's continue our journey into LLMs and Gemini! In the previous article, we moved beyond simple text generation and learned: - how to force the model to speak our language using **structured outputs** (JSON schemas) - how to connect the model to our actual code and logic using **function calling** (tool use, foundation of AI agents) - how to build applications that can make decisions based on user input In fact, we even already made our first steps in the world of **Generative UI** by rendering a dynamic form based on a generated schema. > Note: If you haven't read the previous article, but are confident with concepts like JSON schemas and tool calling, feel free to proceed. Otherwise, I'd suggest reading the previous one [here](https://www.angularspace.com/gemini-and-angular-part-ii-structured-outputs-and-tool-calls/) This time, let's take a significant step forward. We are going to go beyond simple forms and bring full-blown AI capabilities directly into the user interface. ## Our goals In the last article, we saw how useful it is to generate UI elements (like that form) on the fly. However, manual implementation of such dynamic rendering can get complicated quickly. We want rich, interactive interfaces generated in real-time without the boilerplate. We also want more capabilities in terms of visualizing data and *actually* improving user experience. To do this, we will dive deep into **Generative UI**. We will explore how to combine the power of Gemini with **Nano Banana Pro**, the state-of-the-art image generation model by Google to render Angular components dynamically based on the model's responses. We will do our best to write as little code as possible for the most possible return. In the meantime, we can also use this as an opportunity to learn about Angular's newest feature: signal forms, whose highly dynamic nature plays incredibly well with Generative UI scenarios. > Note: throughout this series, we often use cost-effective models like Gemini Flash to keep things accessible for learners. For production-grade Generative UI, using capable models is crucial, so always balance performance with your budget. Let's start! ## Visualizing user preferences to help make decisions Imagine we are developing a car dealership application. We examine the user journey and realize users spend a lot of time thinking about their future car's design and outwards look. We could, of course, write a complicated 3D rendering component and let users customize colors and shapes. While that would be a great approach, it will still come with some downsides: - it would take a lot of time and effort to build such a component - it would be **too** limited to predefined options and styles - still won't allow the user to see their potential car in different environments (will my offroad car look cool when I ride in the mountains?) Instead, we can use Nano Banana Pro to generate images of the car as the user themselves describe and want to see it. This way, we can provide a much richer experience with minimal effort. Before we proceed, let us, however, also outline the pros and cons of this approach, to be able to determine which one is more suitable for our use cases in the future. *Pros*: - **Flexibility**: Users can describe any design they want, and the model can generate it. - **Ease of Implementation**: No need to build complex components to render 3D models - **Rich Visuals**: The model can generate high-quality images that can be more appealing than simple renderings. - **Limits still apply**: While the model can generate *anything*, it is nice that we can still constraint it to a specific domain (cars, only specific models and styles, and so on), making it easier to control outputs. *Cons*: - **Cost**: Image generation models can be expensive to use, especially at scale, and *especially* with Nano Banana Pro, which is the frontier of image generation models as of today. - **Latency**: Generating images can take longer than rendering predefined components or 3D models, leading to potential delays in user experience. - **Quality Control**: The generated images may not always meet user expectations, leading to dissatisfaction With this in mind, let us actually implement this feature in our Angular application, also applying signal forms! ### Implementation First, let us do the, funnily enough, easy part: asking the Gemini API to generate the image based on user's description. For this, we will add a simple Express.js endpoint to our backend. > Note: if you don't have an Express.js backend yet, you can follow the instructions from my [first article](https://www.angularspace.com/building-ai-powered-apps-with-angular-and-gemini/) of this series to get a simple Express backend up and running quickly. > Warning: Nano Banan Pro is available only for the Paid tier of Gemini API access. To access it, you will need an API key that is tied to a Google Cloud Platform account with credits of a valid credit card. ```javascript app.post('/car-image', async (req, res) => { const info = req.body; try { const response = await genAI.models.generateContent({ // Nano Banana is the code name of the image mode, but the actual model name is "gemini-3-pro-image-preview" model: "gemini-3-pro-image-preview", contents: "Generate a photo of a car that adheres to these specific parameters: " + JSON.stringify(info), config: { tools: [{ googleSearch: {} }], imageConfig: { aspectRatio: "16:9", imageSize: "4K" // possible values are 1K, 2K, and 4K }, } }); const inlineData = response.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData; const base64String = inlineData?.data; const mimeType = inlineData?.mimeType; if (!base64String || !mimeType) { res.status(500).json({error: 'Failed to generate image'}); } return res.json({image: `data:${mimeType};base64,${base64String}`}); } catch { res.status(500).json({error: 'Failed to process the request'}); } }) ``` > Note: the `googleSearch` tool is only available for Gemini 3 class models, so you won't be able to use it with earlier models like Gemini 2.5 Flash. Let's do a quick breakdown of what is happening here: - We ask Gemini to use the image generation model (`gemini-3-pro-image-preview`) - We take user's input and add it to a very simple prompt and send it to Gemini - We specify some image configuration (aspect ratio and size). *Careful*: larger images are more costly and take longer to generate! - Finally, we extract the base64-encoded image from the response and send it back to the client An important novelty here is the usage of the `tools` field in the config, particularly the `googleSearch` tool. We explored tool calling in the previous article, but here we are using it in a slightly different way. Instead of providing a tool we have in our own app, we instruct Gemini to use a built-in Google search tool, which allows the model to look up recent images and information on the web to improve the quality of the generated image. For instance, this way we won't have to pass a lot of information about what a given car model should look like - the model can simply look it up itself! This seems fairly straightforward! Now, let us also add a method in our `GenAIService` to call this endpoint from Angular: ```typescript type CarImageDetails = { color: string; background: string; cameraAngle: string; make: string; model: string; year: number; } const BASE_URL = 'http://localhost:3000'; @Injectable({providedIn: 'root'}) export class GenAIService { readonly #http = inject(HttpClient); // other methods omitted for brevity generateCarImage(details: CarImageDetails) { return this.#http.post<{image: string}>(${BASE_URL}`/car-image`, {details}); } } ``` Now, let us move on to the implementation of the actual component's TS side logic. In order to do this, we will use `rxResource` for calling the image generation endpoint and managing the state of the generated image, and a simple signal form to capture user input. ```typescript @Component({/* */}) export class CarComponent { readonly #genAI = inject(GenAIService); imageDetails = signal({ color: '', background: '', cameraAngle: '', make: '', model: '', year: 2000, }); carMakers = ['Toyota', 'Ford', 'Honda', 'Chevrolet', 'BMW', 'Nissan', 'Tesla'] as const; carModelsRaw: Record = { Toyota: ['Camry', 'Corolla', 'Prius'], Ford: ['F-150', 'Mustang', 'Explorer'], Honda: ['Civic', 'Accord', 'CR-V'], Chevrolet: ['Silverado', 'Malibu', 'Equinox'], BMW: ['3 Series', '5 Series', 'X5'], Nissan: ['Altima', 'Sentra', 'Rogue', 'Pathfinder'], Tesla: ['Model S', 'Model 3', 'Model X', 'Model Y'], }; carModels = computed(() => { const make = this.imageDetails().make as typeof this.carMakers[number]; return make ? this.carModelsRaw[make] : []; }); form = form(this.imageDetails, path => { required(path.make); required(path.model); required(path.background); }); generatedImage = rxResource({ stream: () => this.#genAI.generateCarImage(this.imageDetails()), defaultValue: {image: ''}, }); } ``` Here, we can see several quite amazing things going on that were only recently made possible in Angular thanks to signal forms and resources: - Car Model dropdown values are dynamically updated based on the selected Car Make using a computed signal - Form validation is declaratively defined using the `required` function in a signal form - The generated image state is managed using `rxResource`, which handles loading and error states automatically Finally, let us implement the template for this component, which should be relatively simple: ```html
Generated Car Image:
@if (generatedImage.isLoading()) {
} @if (generatedImage.error()) {

Error generating image: {{generatedImage.error()}}

} @if (generatedImage.hasValue() && generatedImage.value().image) { Generated Car Image }
``` As we can see, nothing complex is happening here, since most of the logic is encapsulated inside the signal form and the resource. We simply bind the form controls to the signal form and display the generated image based on the resource's state. > Tip: if you're not fully familiar with Angular Resources, I recommend reading on of my past articles on the topic [here](https://www.angularspace.com/meet-http-resource/). If you are not caught up with signal forms yet, check out this fantastic article from Manfred Steyer: [All About Angular’s New Signal Forms](https://www.angulararchitects.io/blog/all-about-angulars-new-signal-forms/?ref=angularspace.com), or take a look at two of my recent livestreams where I build with signal forms: [Part 1](https://www.youtube.com/watch?v=4EGh0%5FFS9KY&ref=angularspace.com) and [Part 2](https://www.youtube.com/watch?v=qdk%5FHpyYECE&ref=angularspace.com). Alternatively, you can just read the official documentation [here](https://angular.dev/guide/forms/signals/overview?ref=angularspace.com) for a quick catching-up. Now, before we move on, let's quickly see how this component worked out. I live in Armenia and drive a 2008 Nissan Pathfinder, often in the mountains, so maybe let us try and generate a hypothetical image of my car :) ![GIF showing the UI and how the image is being generated](./pathfinder.gif) Wow, looks and works pretty well! Let us now move on to a more complex scenario. ## Step-by-step UI generation Let's think about a consumer scenario that we all find ourselves in quite often: we have an everyday issue (the car won't start, the milk smells bad), we open an LLM Chat like Gemini, ask for help. We then get hit with a wall of text that contains lots of steps and also clarifying questions. We want to answer the questions to get a better response, but some of them are ambiguous, or we are not sure about options. In the end, we want short snippets of steps to do, and start doing more and more prompting, but might still end up with a disappointing result. So, maybe let's solve this issue by creating a highly dynamic UI where the LLM can ask clarifying questions, which will be presented as form controls (with dropdown options when necessary!) that the user will fill in, and then be given the final steps as UI cards to solve their issue. Sounds like a great case for structured outputs and good old prompting! Let's implement this. ### Implementation First, of course, we need to define a new Express.js endpoint that will handle our step-by-step UI generation. This one is more complex than the previous, so we will go through it step by step. First, let us talk about what sort of response we want to expect from Gemini. We will use structured outputs to define a schema that contains two main parts: ```javascript const schema = { "type": "object", "properties": { "steps": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string" }, "text": { "type": "string" } }, "propertyOrdering": [ "title", "text" ], "required": [ "title", "text" ] } }, "form": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "select", "number" ] }, "options": { "type": "array", "items": { "type": "string" } }, "question": { "type": "string" } }, "propertyOrdering": [ "type", "options", "question" ], "required": [ "type", "options", "question" ] } } }, "propertyOrdering": [ "steps", "form" ] } } ``` If this looks intimidating, it might be way easier to look at the TypeScript type that corresponds to this schema: ```typescript export type ControlFieldType = 'text' | 'select' | 'number'; export type ControlSchema = { type: ControlFieldType; options?: string[]; question: string; } export type Step = { title: string; text: string; } type SchemaResponse = { steps?: Step[]; form?: ControlSchema[]; } ``` As we can see, we either expect Gemini to instruct us to show controls with questions for the user if any new info is necessary, or give us concrete steps to solve the issue if the user input is sufficient. So, here's how the endpoint will look like: ```javascript app.post('/fix-it', async (req, res) => { const { query, additionalInfo } = req.body; const prompt = `The user will provide an issue they are facing in their day-to-day life. You task is to find a solution and present it in actionable steps. If additional information is not provided, and knowing that additional information will help find the solution steps, return a list of items the user has to respond to. Those will be presented to the user as UI elements like dropdowns or inputs where they will input necessary information for you to provide a solution. User query: \n\n${query}, additional info ${additionalInfo ? JSON.stringify(additionalInfo) : 'not provided'}`; try { const response = await genAI.models.generateContent({ model: "gemini-3-pro-preview", contents: prompt, config: { tools: [{ googleSearch: {} }], responseMimeType: 'application/json', responseJsonSchema: schema } }); res.json(JSON.parse(response.candidates[0].content.parts[0].text)); } catch { res.status(500).json({error: 'Failed to process the request'}); } }); ``` As we can see, we leaned way harder into prompt engineering here, providing additional context when it is available, and using the same Google Search tool to help the model find relevant information on the web if necessary. We also again strictly required a structured JSON response based on our schema. Let's quickly add a new method to our `GenAIService` to call this endpoint: ```typescript const BASE_URL = 'http://localhost:3000'; @Injectable({providedIn: 'root'}) export class GenAIService { readonly #http = inject(HttpClient); // other methods omitted for brevity solveIssue( data: {query: string, additionalInfo?: Record} ) { return this.#http.post<{form?: ControlSchema[], steps?: Step[]}>(`${BASE_URL}/fix-it`, data) } } ``` Now, let's stop for a moment and think about what we did here: instead of two endpoints, one for clarifying additional information, and one for the actually answering the question, we created a single endpoint that can do both! While this helps us keep our codebase slightly cleaner, and maybe avoids scenarios where the model still asks for more information even when the user provided everything within their query, it also means we will have to handle more complex logic on the client side. This approach in no way is "better" than the two-endpoint approach, so make sure to evaluate your use case and choose accordingly. Now, to have our actual UI, it would be best for us to split it into three components: one that receives the form schema and renders the necessary controls, one that receives the steps and displays them as cards, and one parent component that manages the state and orchestrates calls to the backend. Let's start with the form component: ```typescript @Component({ selector: 'app-dynamic-form', standalone: true, imports: [Field], template: ` @if (schema().length > 0) {
@for (control of schema(); track $index) {
@switch (control.type) { @case ('text') { } @case ('number') { } @case ('select') { } }
}
} `, }) export class DynamicFormComponent { schema = input.required(); submit = output>(); formValue = linkedSignal(() => { const s = this.schema(); const group: Record = {}; s.forEach(c => group[c.question] = ''); return group; }); dynamicForm = form(this.formValue); onSubmit() { const additionalInfo = this.dynamicForm().value() this.submit.emit(additionalInfo); } } ``` Here, the interesting part is actually the `linkedSignal`, which is dynamically created from the input schema, but is still mutable by itself unlike a `computed`, so then we can easily pass it to the signal form. The rest is fairly straightforward dynamic form rendering. Also please note that we use `$any` in the template here a lot, simply driven by the fully dynamic nature of this component (we have no idea what inputs the LLM might make us render here). After the user fills in, they click the button and we emit the filled-in values to the parent component. Next, let us implement the steps component: ```typescript @Component({ selector: 'app-cards-stepper', standalone: true, template: ` @if (steps().length > 0) {
@if (hasPrevious()) { } @if (steps()[currentIndex()]) {

{{ steps()[currentIndex()].title }}

{{ steps()[currentIndex()].text }}

} @if (hasNext()) { }
} `, }) export class CardsStepperComponent { steps = input([]); stepChange = output(); currentIndex = signal(0); hasPrevious = computed(() => this.currentIndex() > 0); hasNext = computed(() => this.currentIndex() < this.steps().length - 1); prev() { if (this.hasPrevious()) { this.currentIndex.update(i => i - 1); this.stepChange.emit(this.currentIndex()); } } next() { if (this.hasNext()) { this.currentIndex.update(i => i + 1); this.stepChange.emit(this.currentIndex()); } } } ``` While this component might seem a bit complex, it is actually quite straightforward: we simply display the current step, and if available, the previous and next steps as cards. The user can navigate between steps using arrows or by clicking on the cards themselves. Finally, let us implement the parent component that will orchestrate everything, here is the actually interesting logic going on: ```typescript @Component({ selector: 'app-fix-it', template: `

Fix your issue

@if (result.isLoading()) {
} @if (result.hasValue() && result.value()) {
@if (result.value().form) { } @if (result.value().steps) { }
}
`, imports: [DynamicFormComponent, CardsStepperComponent, Field] }) export class FixItComponent { readonly #genAI = inject(GenAIService); controls = signal<{ query: string, additionalInfo?: Record }>({query: ''}); form = form(this.controls, path => { required(path.query); }); result = rxResource({ stream: () => { const {query, additionalInfo} = this.form().value(); if (this.form().invalid()) { return of(undefined) } return this.#genAI.solveIssue({query, additionalInfo}); } }); resubmit(additionalInfo: Record) { this.controls.update(c => ({...c, additionalInfo})); this.result.reload(); } } ``` here we have a simple form with one input for user's query, then we trigger a resource reload with out Gemini API call. We might either get the steps or a form schema in response, and we render the corresponding component accordingly. If we get a form schema, we also pass a handler to capture the submitted additional info and trigger another reload with the new info. Angular signals, forms, and resources make this incredibly easy to implement! So, as with the previous example, here is how this component works in practice: ![GIF showing the UI and how the step-by-step generation works](./fix-it.gif) And we are done! ## Conclusion In this article, we deepened our understanding of Generative UI by combining Gemini's text generation capabilities with Nano Banana Pro's image generation power, meaning we officially made our first steps into multimodality. Creating generative UIs is part and parcel of building AI-powered applications, and in my own opinion GenUI-s will become more and more prominent and widespread as the entire field progresses. In the next article, we will explore embeddings - a powerful concept that allows us to build so much more than just generative experiences, but rather incorporate semantic searches, recommendations, and knowledge-based applications like RAGs (retrieval-augmented generation). Stay tuned! ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) P.S There is one chapter in my book that helps you work LLMs in the context of Angular apps; that chapter is already kind of outdated, despite the book being published just earlier this year (see how insanely fast-paced the AI landscape is?!). I hope you can forgive me ;) ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/01/Generated-Image-January-22--2026---2_15PM.jpeg) [ ![Angular University - High Quality Angular Courses](https://d3vigmphadbn9b.cloudfront.net/banners/angular-university-banner-4.jpg) ](https://angular-university.io/?ref=angularspace.com) ### Signal Forms URL: https://www.angularspace.com/signal-forms/ Last updated: 2026-01-21T09:23:54.000Z Experimental - This is intended for use in non-production applications, as the API can change (without notice) as it did with 21.0.0-next.8 where Control was renamed to Field. Signal Forms is one of the most talked-about features to be added to the Angular framework, and we recently got to see an experimental version of this with the recent beta release of Angular version 21\. Signal Forms will be a game-changer when it comes to developing forms. Not only does it simplify form creation by removing a significant amount of boilerplate code that Template and Reactive Forms require, but it has also made form validation, form submission and creating custom controls significantly easier. Signal Forms are `model driven`, we create a `signal` model and then pass this to the `form` function as an argument. ```typescript protected readonly userProfile = signal({ // model properties }) ``` For example, in the past when creating custom controls that will be used in our forms, we've had to implement the ControlValueAccessor interface to allow our custom controls to integrate with the Forms or Reactive Forms Module, the good news is this has been greatly simplified as well, along with a few other issues we faced when creating custom controls, which we'll look at in this post. Let's walk through setting up a Signal Form, we start off by creating our model. ```typescript type UserProfile = { firstName:string; lastName:string; phone:string; email:string; } ``` Next, let's build our form, we create a signal of our model and then using the new `form` function to create a wrapper around our model. ```typescript export class User { protected readonly userProfile = signal({ firstName:'', lastName:'', phone:'', email:'', }) protected readonly userForm = form(this.userProfile); } ``` That's all we need to create a Signal Forms - how easy was that compared to Template or Reactive forms. Signal Forms are represented as a `FieldTree`and `FieldState` this is a hierarchical structure of our form and looks like this. ```typescript // our model type UserProfile = { firstName:string; lastName:string; phone:string; email:string; } // user (FieldTree root) // ├─ firstName (FieldState) // ├─ lastName (FieldState) // ├─ phone (FieldState) // └─ email (FieldState) ``` If we had a nested model it would be represented like this: ```typescript // our model type userProfile = { firstName: string; lastName: string; phone: string; email: string; address: { street: string; city: string; } } // user (FieldTree root) // ├─ firstName (FieldState) // ├─ lastName (FieldState) // ├─ phone (FieldState) // ├─ email (FieldState) // └─ address (FieldTree node) // ├─ street (FieldState) // └─ city (FieldState) ``` The main difference of Signal Forms compared to Template or Reactive forms is that Signal Forms doesn't maintain a copy of the data, so when we update a `FieldState` in the tree, we are directly mutating the original model. A `FieldState` represents an individual form field including it's state (value, validity, dirty status etc...). Now, let's have a look at connecting our input components to our new form. I'm using Angular Material in these examples. ```html First name ``` To connect our model and template, we need to use the new \[field\] method, passing in a field that we want this input to be bound to. This is nice and simple, and we get two-way data binding out of the box. In version 21.0.0-next.8 \[control\] was renamed to \[field\]. ```html // for reference - the array for the dropdown to iterate over // address: [] = [ // { value: '0', viewValue: 'Primary' }, // { value: '1', viewValue: 'Billing' }, // { value: '2', viewValue: 'Shipping' }, // ]; Address @for (addr of address ; track address.value() ) { {{addr.viewValue}} } ``` Setting up a drop-down list is just as easy. Let's now look at validation. The `form` function takes a second parameter, which can be a schema or a function, or form options (if a schema is passed as the second argument, the form options can be passed in as a third option). ```typescript // recap what our model looks like type UserProfile = { firstName:string; lastName:string; phone:string; email:string; } protected readonly userForm = form(this.userProfile, (path)=>{ required(path.firstName), required(path.lastName), email(path.email) }); ``` This second parameter is a function and takes a `fieldPath` as an argument, in this function we set up our validation. The ordering in which validation is applied doesn't matter. The built-in validation is now imported from `forms/signals` and we have a similar list two what we have in Template or Reactive Forms: - Email - Max - MaxLength - Min - MinLength - Pattern - Required With the validation, we set the path to the `fieldState` we want the validation applied to. In the HTML we just need to iterate over the errors object. ```html Email address @if(profileForm.email().errors().length > 0) { @for(error of profileForm.email().errors(); track error) {
Error message goes here...
}
}
``` Adding error messages like this could get a bit long with multiple error messages, so to help with this, we can add a message to the `form` like this: ```typescript // recap what our model looks like type UserProfile = { firstName:string; lastName:string; phone:string; email:string; } protected readonly userForm = form(this.userProfile, (path)=>{ required(path.firstName, {message: 'This is a required field.'}), required(path.lastName, {message: 'This is a required field.'}), email(path.email, {message: 'The email address is not valid.'}) }); ``` And in the HTML we can read the `error.message` like this: ```html @if(profileForm.email().errors().length > 0) { @for(error of profileForm.email().errors(); track error) { {{ error.message }} } } ``` Because I'm using Angular Material (and this might just because it's still experimental at present), but to get the `mat-error` to work and display the error message correctly under the `input` I had to wrap an `@if` block around the `mat-error`, hopefully this will not be the case in later releases but is necessary at the moment. I don't like repeating code unnecessarily, and the validation we have just created repeats (albeit, just twice in our example), but imagine if you have three, six, nine, or more controls; the that's going to be repeated a lot. Luckily, there is another method that we can use to remove the duplication. For this, we need to create a `schema`, which can then be applied to our `form`. Let's adjust our code and create a schema. ```typescript const profileSchema: Schema = schema((path) => { required(path, { message: 'This is a required field.' }); minLength(path, 3, { message: 'This needs to be more than three characters'}); }); ``` ```typescript protected readonly userForm = form(this.userProfile, (path)=>{ apply(path.firstName, profileSchema); apply(path.lastName, profileSchema ); email(path.email, {message: 'The email address is not valid.'}) }); ``` We use the `apply` function to apply our schema to the fields we want to validate. This will apply both `required` and `minLength` to the `firstName`, and we can create multiple `schemas` as necessary, for example we only want `minLength` validation on certain controls. ## Custom validation We can also create custom validators as well, let's create a custom validator that makes the phone number control contains numbers only. We need to create a function that takes a path and an optional options ```typescript export function numericOnly( path: FieldPath, options?: { message?: string } ): void { validate(path, (ctx) => { const value = ctx.value(); if (!/^\d+$/.test(String(value))) { return customError({ kind: 'phone', value: true, message: options?.message || 'Phone must contain only numbers.', }); } return customError({ kind: 'phone', value, }); }); ``` ```typescript protected readonly userForm = form(this.userProfile, (path)=>{ // other validators numericOnly(path.phone); }); ``` ## Conditional validation Signal Forms has you covered for that as well. Let's adjust our model, lets add a `emailMarketing` flag to our model, so that the validation for email is only applied if the `emailMarketing` checkbox is ticked (true). ```typescript type UserProfile = { firstName:string; lastName:string; phone:string; email:string; emailMarketing: boolean; } protected readonly userForm = form(this.userProfile, (path)=>{ required(path.email, { when: ({ valueOf }) => valueOf(path.emailMarketing) === true, message: 'This is a required field.', }); email(path.email, {message: 'The email address is not valid.'}) }); ``` We'll add a `required` validator and set a path to email, in the configuration options there is a `when` property and we can use this to check the `valueOf` another field, in our case we what to apply the validation `when` the emailMarketing checkbox is ticked. If you have applied a `required` validation schema you will need to remove this as it will also be applied. ## Form Submission When it comes to submitting our form to the server, we have a new function called... (you've guessed it) `submit`. This function takes two arguments: the first is our `form` and the second is a function that returns a `promise` or `undefined` if the save to our back end is successful. If it's not successful, we return an array of objects, and within this object, we can set the `kind` of error here; we're specifying `server` , if we want to attach the error to a specific control, we specify the `field`; and finally, we set the `error` message to be displayed. ```typescript onSubmit() { submit(this.userProfile, async (form) => { try { this.userProfileService.saveForm(form); // call to API to save our form data this.userProfile().reset(); return undefined; } catch (error) { return [ { kind: 'server', field: this.profileForm.firstName, message: (error as Error).message, }, ]; } }); } ``` Calling the `.reset()` on the form only resets the `pristine`, `dirty` and `touched` to reset the form values after submitting reset the model values. When submitting a form, we usually disable the `save` button while this operation takes place. The `form()` function exposes a `submitting()` signal that we can use to disable our `save` button. ```html ``` In Reactive forms we would set up our forms like: `
...
` at present this isn't fully fleshed out in Signal Forms, there is a some information on the [road-map](https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=127969774&ref=angularspace.com) about it and possible solutions. ## Custom controls When creating custom controls we no longer need to implement the `ControlValueAccessor` interface, we now have a simpler new interface, the good news is we only need to implement one property and not four methods as before, this new interface is called `FormValueControl<>` and we need to set the `value` property our component (and it must be called value), which must be a `model()`. Let's create an example: ```typescript // our component import { Component, model } from '@angular/core'; import { FormValueControl } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'star-rating', imports: [MatIconModule], template: ` @if(required()){ * }
@for (star of stars; track $index) { {{ (hoverRating >= star || value() >= star) ? 'star' : 'star_border' }} }
`, }) export class StarRatingComponent implements FormValueControl { value = model(0); disabled = input(false); required = input(false); stars = [1, 2, 3, 4, 5]; hoverRating = 0; setRating(rating: number) { if (!this.disabled()) { this.value.set(rating); } } } ``` With the new `FormValueControl` interface, we just need to include the `value` property in our component; it also has to be of type `modelSignal`, and that's all we need to set. If we look at the `FormValueControl` interface we can see that it extends [FormUiControl](https://next.angular.dev/api/forms/signals/FormUiControl?ref=angularspace.com) this provides many optional properties for instance, [required](https://next.angular.dev/api/forms/signals/required?ref=angularspace.com) and [disabled](https://next.angular.dev/api/forms/signals/disabled?ref=angularspace.com) and for our component to make use of these we just need to include them in our component and in the parent component we just need to set the states in the `form`. ```html ``` ```typescript type ProductProfile = { name:string; description:string; price:number; rating:number; leaveReview: boolean; } protected readonly productProfile = signal({ name:'', description:'', price:0, starRating:0, leaveReview: false }) protected readonly productForm = form(this.productProfile, (path) => { required(path.starRating), disabled(path.starRating,({valueOf})=> valueOf(path.leaveReview) === true) }); ``` This is all we need to have the `required` and `disabled` properties for our form to work as we'd expect, the `starRating` is in a `disabled` state until the `leaveReview` is set to `true`. # Conclusion Signal Forms is going to be a game-changer for creating forms in Angular applications when it's released. Hopefully, this post has given some insight into how to use it. I've been really impressed by how complete it is (even though it's currently experimental), and over the next few weeks and months, this will only get better. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2026/01/image.png) [ ![Angular University - High Quality Angular Courses](https://d3vigmphadbn9b.cloudfront.net/banners/angular-university-banner-4.jpg) ](https://angular-university.io/?ref=angularspace.com) ### Gemini and Angular, Part II: Structured Outputs and Tool calls URL: https://www.angularspace.com/gemini-and-angular-part-ii-structured-outputs-and-tool-calls/ Last updated: 2025-11-06T13:43:39.000Z Let's continue our journey into LLMs and Gemini! In the previous article, we learned - how LLMs generate text, what are tokens, what configuration parameters like temperature, `topP` and so on mean - how to create a Google Cloud project, get a Gemini API key, and use the SDK to make text generation requests - how to leverage the different models for different tasks > Note: If you haven't read the previous article, but are confident of the knowledge you already have in regards to the topics mentioned above, feel free to proceed reading this one. Otherwise I'd suggest to first read the previous one [here](https://www.angularspace.com/building-ai-powered-apps-with-angular-and-gemini/) This time, let's move forward and build even more complicated things, while still requiring only marginally more knowledge outside of what a typical Angular developer will possess. ## Our goals After being able to actually generate text in response to a prompt, we might be tempted to jump on and try to "build a chatbot". However, as exciting as it is, one of our goals with these articles will be to show how much more LLMs are actually capable of, rather than simply being "engines for chatbots". To do this, we will, in depth, learn about two important concepts: *structured outputs* and *function calling* (also known as *tool use*). With these tools, we can build applications that can make AI-powered decisions affecting the UI and UX of our applications, and lay the foundation for our future explorations of a powerful frontend + LLM approach known as generative UI. > Note: throughout this and other articles of this series, slightly older models like Gemini Flash 2.0 Flash Lite are used, with the sole goal of reducing costs for readers/learners. For better results it is recommended to use the latest models available in the Gemini family, however, of course, taking into consideration the cost implications. Let's start! ## Structured outputs As always, I believe it is best to start with a task at hand, and then see how the tools we are going to explore will fit into solving that task. Let's build a writing assistant, that will help writers come up with better wording for certain paragraphs. First of all, let us build a simple Angular component, which will present the user with two `textarea` inputs, and a button. The user will input two versions of the same paragraph, and the AI will help them pick the better one. When the user clicks the button, we will send both paragraphs to the Gemini API, and ask it to provide an overview of which one is better and why, then display that overview in a separate box. ```typescript @Component({ selector: 'app-writing-assistant', template: `

Writing Assistant

@if (overview.hasValue()) {

Comparison Result:

{{ overview.value() }}

}
`, imports: [FormsModule], }) export class WritingAssistantComponent { readonly #genAI = inject(GenAIService); paragraph1 = signal(''); paragraph2 = signal(''); overview = rxResource({ stream: () => { if (!this.paragraph1() || !this.paragraph2()) { return of(''); } return this.#genAI.writingOverview(this.paragraph1(), this.paragraph2()); }, }); } ``` As we can see, a new method named `writingOverview` is being called on the `GenAIService`. To implement it, we do not actually need a new endpoint on our simple backend, since we already implemented a generic `/generate-text` endpoint. All we need to do is to call the method in the service providing the two paragraphs and a prompt. ```typescript writingOverview(paragraph1: string, paragraph2: string) { const prompt = `Provide a brief overview comparing the following two paragraphs:\n\nParagraph 1: ${paragraph1}\n\nParagraph 2: ${paragraph2}. Decide which version is better\n\nOverview:`; return this.generateContent(prompt); } ``` Now, when we see the two inputs, we can put "Hello, how are you?" and "hlo hwo aer yo" to test a very clear cut example, and then, in our overview box, we can see something like the following: ``` **Overview:** Paragraph 1 is a standard, grammatically correct greeting. Paragraph 2 is a heavily abbreviated and misspelled version of the same greeting. **Decision:** Paragraph 1 is significantly better due to its clarity, proper grammar, and readability. Paragraph 2 is difficult to understand and would be considered unprofessional in most contexts. ``` This is absolutely great! But what if, instead of just showing a response text, we wanted to actually change our UI in accordance to the response? Like, it would be nice from the UX perspective if we could highlight the better paragraph in green, and the worse one in red. But how do we achieve it? I mean, from the text we got, it is pretty obvious that paragraph 1 is the better choice, but how do we translate it into code? Here's where structured outputs come into play. It would surely be great if instead of plain text, Gemini returned as a JSON of, for example, this form: ```json { "betterParagraph": 1, "reason": "Paragraph 1 is significantly better due to its clarity, proper grammar, and readability. Paragraph 2 is difficult to understand and would be considered unprofessional in most contexts." } ``` But how do we make it work like this? Surely, we can write our prompt in a way that it *asks* for a JSON response. However, we might quickly become quite frustrated, since the model will often start the message with "parasite" words like "Sure. here;s your JSON:", or it will simply return a plain text response, or it will return a malformed JSON. Fighting this with prompt engineering might yield some results, but is not worth the time, and might not be fully reliable anyway. Instead, we can use a feature of the Gemini API called *structured outputs*. It allows us to define a schema for the response we expect, and the model will *guarantee* that the response will be in the correct format! To do that, we need to do the following: - define a new endpoint to call Gemini - provide a response type ("application/json" for our use case) - provide the schema (what fields we expect, and what types they are) Here's a simple implementation: ```javascript app.post('/writing-assistant', async (req, res) => { const { paragraph1, paragraph2 } = req.body; const schema = { "type": "object", "properties": { "overview": { "type": "string" }, "bestChoice": { "type": "string", "enum": ["1", "2"] } }, "required": ["overview", "bestChoice"] }; const prompt = `Provide a brief overview comparing the following two paragraphs:\n\nParagraph 1: ${paragraph1}\n\nParagraph 2: ${paragraph2}. Decide which version is better\n\nOverview:`; try { const response = await genAI.models.generateContent({ model: 'gemini-2.0-flash-lite', contents: prompt, config: { responseMimeType: 'application/json', temperature: 0.1, responseSchema: schema, }, }); res.json(response.candidates[0]?.content.parts[0].text || {}); } catch (error) { console.error('Error generating writing overview:', error); res.status(500).json({ error: 'Failed to generate writing overview' }); } }); ``` As we can see, most of it is the same boilerplate as previously, with two slight differences: 1. We defined a `schema` object, which describes the structure of the response we expect 2. In the `config` object, we provided two new properties: `responseMimeType`, which we set to `"application/json"`, and `responseSchema`, which we set to the schema we defined. The schema object is pretty straightforward. It is a standard JSON schema, which you can learn more about [here](https://json-schema.org/understanding-json-schema/?ref=angularspace.com). In our case, we expect an object with two properties: `overview`, which is a string, and `bestChoice`, which is also a string, but can only be one of two values: "1" or "2". Both properties are required. Now, we can modify out Angular service to call this new endpoint: ```typescript writingOverview(paragraph1: string, paragraph2: string) { return this.#http.post< GeminiResponse >('http://localhost:3000/writing-assistant', {paragraph1, paragraph2}); } ``` As we can see, the only difference is that instead of simply extracting the text from the response, we parse it as JSON before returning from the backend. This is of course made possible by the structured output we used to instruct Gemini on how to generate the response. Finally, we can modify our component to use the structured response to change the UI: ```typescript @Component({ selector: 'app-writing-assistant', template: `

Writing Assistant

@let value = overview.value(); @if (value.overview) {

Comparison Result:

{{ value.overview }}

}
`, styles: ` .better { border: 2px solid green; } .worse { border: 2px solid red; } `, imports: [FormsModule], }) export class WritingAssistantComponent { /* rest of the component code remains unchanged */ } ``` > Note: if you're curious about *how* LLMs are capable of generating structured outputs so precisely, watch this [YouTube video](https://www.youtube.com/watch?v=xpvFinvqRCA&ref=angularspace.com) for a very detailed explanation; however, do not worry if you do not understand all the nuances, it is not required to know how structured outputs work to effectively use them Now we do not only display the overview, but also highlight the better paragraph in green, and the worse one in red! Absolutely amazing and what an intro to both structured outputs and generative UIs! Of course, this example was quite simplistic. Let's drive the point about usefulness of structured outputs even further, since we can achieve quite spectacular things with LLMs that produce coherent responses and decisions. Next, let's try to build a dynamic form. ## Advanced features with structured outputs Have you ever created a custom Google form, perhaps to collect some feedback, or maybe acquire information about potential participants of an event? If you did, the next part is going to be familiar, yet exciting. We are going to build an interface which allows the user to add questions, provide a format for answers (for simplicity we will have "input", "textarea", and "dropdown" with options, but this can easily be expanded), and also see a *live* preview of the form - not simply an image, but an actual form the creator can play around with and edit. To further make this interesting, we will use Angular signal forms, which are poised to enter the scene in v21, and make it extremely simple to create a dynamic form. Finally, our goal is to simplify the process of creating a form by allowing the user to input a description of what they want the form to be about, and then have Gemini generate the questions for them! So, for instance, they might type something like "I want to create a feedback form for my new product", and Gemini will generate a set of questions that would be appropriate for such a form, and we will display the form as it is directly on the same page. > Note: if you are unfamiliar with Angular signal forms, I suggest you read [this article by Manfred Steyer](https://www.angulararchitects.io/blog/all-about-angulars-new-signal-forms/?ref=angularspace.com) first, or watch me live code with signal forms [here](https://www.youtube.com/watch?v=4EGh0%5FFS9KY&ref=angularspace.com). To achieve this, we will need to do the following: - a data type that describes what a custom form looks like - a prompt to generate a form - a schema based on the data type that will be used for structured output - a linked signal derived from the structured output of Gemini - a form created from that linked signal Let's do it step by step. First, let's define a simple data type that describes what a custom form looks like. Since we can have multiple fields, it is reasonable to think of every field descriptor as an object, and the entire form as an array of such objects. Here's what our object might look like: ```typescript export interface FormField { name: string; type: 'input' | 'textarea' | 'dropdown'; options?: {value: string, label: string}[]; // only for dropdown } ``` Very good, now, we need to define a schema for this object. We remember how to do it from the previous example, however, it can be quite cumbersome, and we might inadvertently make some mistakes. So, instead, what we are going to do is head over to [Google AI Studio](https://aistudio.google.com/?ref=angularspace.com), toggle "Structured Output" on, and click "Edit". In the open popup, we can click on "Visual Editor" and start adding our properties and defining values! We can add properties, define their types, add enum values for strings, and more. Here's what our schema will look like visually: ![Schema visual editor](./ai-studio-screenshot.png) As we can see, all the fields are defined, so what is left is to switch to "Code Editor" and simply copy paste the generated schema into our backend code: ```javascript app.post('/generate-form', async (req, res) => { const { prompt: query } = req.body; if (!query) { return res.status(400).json({ error: 'Prompt is required' }); } const prompt = `User will provide a description of a generic form, and you will generate the blueprint. Include only the fields required to add data, do not include buttons. ${query}`; const schema = { "type": "object", "properties": { "form": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "type": { "type": "string", "enum": [ "input", "textarea", "dropdown" ] }, "options": { "type": "array", "items": { "type": "object", "properties": { "value": { "type": "string" }, "label": { "type": "string" } }, "propertyOrdering": [ "value", "label" ], "required": [ "value", "label" ] } } }, "propertyOrdering": [ "name", "type", "options" ], "required": [ "name", "type" ] } } }, "propertyOrdering": [ "form" ], "required": [ "form" ] }; try { const response = await genAI.models.generateContent({ model: 'gemini-2.0-flash-lite', contents: [ { role: 'user', parts: [{ text: prompt }] } ], config: { responseMimeType: 'application/json', temperature: 0.1, responseSchema: schema, } }); res.json(response.text); } catch (error) { console.error('Error generating form blueprint:', error); res.status(500).json({ error: 'Failed to generate form blueprint' }); } }); ``` It's quite obvious that while it's a bit of a long function, the only two differences from the previous example are the prompt and the schema. The rest is the same boilerplate, which is great news, since it means we can now build a component using this endpoint. ```typescript @Component({ selector: 'app-generative-form', template: `

Generative Form Component

@if (formBlueprint.hasValue()) {

Generated Form Blueprint:

@for (field of formBlueprint.value(); track field.name) {
@switch (field.type) { @case ('input') { } @case ('textarea') { } @case('dropdown') { } @default {
Unknown field type: {{field.type}}
} }
{{formValue() | json}} } } `, imports: [Control, TitleCasePipe, JsonPipe], }) export class GenerativeFormComponent { readonly #genAI = inject(GenAIService); prompt = signal(''); formBlueprint = rxResource({ params: () => ({prompt: this.prompt()}), stream: ({params}) => { if (!params.prompt) { return of(null); } return this.#genAI.generateForm(params.prompt); }, }); formValue = linkedSignal(() => { const blueprint = this.formBlueprint.value(); if (!blueprint) { return null; } const value = {} as Record; for (const field of blueprint) { value[field.name] = ''; } return value; }); form = form(this.formValue); } ``` Now, let's carefully examine what we have done here 1. Create a resource that calls the `generateForm` method of our service, then stores the value of the form as the array we mentioned 2. Create a linked signal that derives its value from the form blueprint, and creates an object with keys being the names of the fields, and values being empty strings. This will be the base of the signal form 3. Create a signal form from the linked signal using the `form` function; yes, this is *that* simple with signal forms! 4. In the template, iterate over form fields and define appropriate controls based on the type of the field using `@switch`/`@case` blocks 5. To be able to dynamically read form controls and bind them to inputs with the `[control]` directive, we need to use `$any`, since the form structure is not known at compile time (obviously, it is generated with an LLM) And this is it! Now, the end user can simply type the description of the form they want to create, live preview it and play around, edit, iterate, and get to their final result! Let's now move to the final topic of this article (which is actually and opening of a whole new world) - function calling. ## Function calling Funnily enough, function or tool calling, while a fundamental pillar of building AI-powered applications and agentic workflows, is a feature that can essentially be thought of as a subset of structured outputs. Let's understand what it is in general and how it (slightly) differs from a structured output. - Structured outputs allow us to define a schema for the response we expect from the model, and the model will generate a response that adheres to that schema - Function calling lets us tell the LLM what functions we have available (like methods in frontend or backend), and the model will decide which one (API, database, search tool) it needs to call - We could theoretically achieve the same result by using structured output, however, with function calling, we can get both output (even structured) *and* function calls, thus being able to not only call functions, but show explainer messages and prompts from the model So, let's start building an app command line, where user can input prompts that wil do commands in our app, like navigating to a certain page, changing settings, and more. The very first thing we will implement is the navigation command. To achieve this, we need to define a schema that describes our function and its parameters. Here's what it might look like: ```typescript [ { "name": "navigate", "description": "Navigates the user to the page defined by the URL", "parameters": { "type": "object", "properties": { "url": { "type": "string", "enum": [ "/some-url", "/another-url", "/yet-another-url" ] } }, "required": [ "url" ], "propertyOrdering": [ "url" ] } } ] ``` As we can see, it is almost identical to what we provided for structured output, with the only difference being that we have a `name` and `description` properties at the top level, which describe the function we are defining. These are of paramount importance, since the LLM will use those parameters to decide which function(s) to call out of many provided. However, this setup is not very useful, since we provided some mock urls, but the URLs that our Angular app actually have are quite different, and also dynamic in the sense that in the future, more routes might become available, or old ones become obsolete. To counter this, the best thing we can do is to generate the schema dynamically, based on the actual routes our Angular app has. To do that, we can use the `Router` service, and extract the routes from it. Here's how we can do it: ```typescript getRoutesSchema() { const routes = this.#router.config .filter(route => route.path) // filter out routes without a path .map(route => `/${route.path}`); // prepend '/' to each path return { name: 'navigate', description: 'Navigates the user the page defined by the URL', parameters: { type: 'object', properties: { url: { type: 'string', enum: routes, }, }, required: ['url'], propertyOrdering: ['url'], }, }; } ``` Before we make this work, let's again think about what is going here. We are creating and object that describes a function that the LLM may or may not (this is important!) choose to invoke. When we say "invoke", in this context we do not mean it will actually invoke it, but rather tell us (our app) to do that (we are still able to opt out of actually doing it). The function is `navigate`, and it takes a single parameter `url`, which is a string, and can be one of the routes we extracted from the Angular Router. So, simply put, the LLM notifies the caller to execute navigate(url) to navigate to a page. Now, we need to add a simple endpoint that will just pick up the user's prompt and the schema we dynamically obtained and call Gemini: ```javascript app.post('/command-line', async (req, res) => { const { commands, prompt } = req.body; if (!commands) { return res.status(400).json({ error: 'Commands are required' }); } const finalPrompt = ` You are an assistants who helps users execute commands in a web app defined in natural language. Take a look at the tools available and the user's prompt and decide which ones to call with what arguments. User prompt: ${prompt} `; const response = await genAI.models.generateContent({ model: 'gemini-2.0-flash-lite', contents: prompt, config: { temperature: 0.1, tools: [{functionDeclarations: commands}], }, }); res.json(response); }); ``` As we can see, this time it's even simpler, as the bulk of the works is done in the frontend, and provided to Gemini as a toolset it can call. Here we also slightly augment the user's prompt to make Gemini focus on choosing a right tool, since it is a command tool rather than a chatbot. Finally, we can implement the component that will provide the user interface for our command line: ```typescript @Component({ selector: 'app-command-line', template: ` `, }) export class CommandLineComponent { readonly #router = inject(Router); readonly #genAI = inject(GenAIService); onEnter(event: Event) { const input = (event.target as HTMLInputElement).value; const commands = [this.getRoutesSchema()]; // callCommandLine simply takes the user's input and the commands schema and calls the /command-line endpoint we defined above this.#genAI.callCommandLine(commands, input).subscribe(response => { const command = response.candidates[0]?.content.parts[0].functionCall; this.handleCommand(command); }); } handleCommand(command?: {name: string; args: Record}) { switch (command.name) { case 'navigate': const url = command.args['url']; // we might want to validate the URL here before navigating // since LLMs can sometimes hallucinate, in a production setting // it would be a good idea to check if the URL actually exists // within our app routes this.#router.navigateByUrl(url); break; default: console.warn(`Unknown command: ${command.name}`); } } getRoutesSchema() { // omitted for the sake of brevity, see above } } ``` As we can see, the `response.candidates[0]?.content.parts[0]` item now contains a `functionCall` property, which is the function Gemini decided needs to be called and passed back to us. We then handle that result in a simple `switch/case` block, and call the appropriate Angular method - in our case, `Router.navigateByUrl`. If we now open the component in a browser and type something like "navigate to writing assistant", the app will navigate to the writing assistant component we built previously! Absolutely amazing. Of course, for now we only have one command, but here is another that you can now implement on your own: a command that changes the theme of the app (light/dark). You can define a simple service that holds the current theme, and then define a function schema for changing the theme, and implement the command in the `handleCommand` method. ## Conclusion We built a lot on top of what we already had in the previous article. We learned: - structured outputs that can help us force the model to generate responses in a certain format, and how to use them to build generative UIs - function calling, which allows us to define functions (or tools) that the model can call - we touched very slightly on prompt engineering, augmenting the user's prompt to make it work better for our use case - we began a journey into agentic workflows, where the model can make decisions and call functions based on the user's input to come up with way more complex results than simply text generation In the next article, we will go super deep and discuss embeddings - special numerical representations of text that allow us to do some absolutely spectacular things, like semantic search, text classification, and more. Stay tuned! ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) P.S There is one chapter in my book that helps you work LLMs in the context of Angular apps; that chapter is already kind of outdated, despite the book being published just earlier this year (see how insanely fast-paced the AI landscape is?!). I hope you can forgive me ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/11/Screenshot-2024-11-13-at-15--1---1---2---1-.jpg) ### Angular Zoneless Unit Testing URL: https://www.angularspace.com/angular-zoneless-unit-testing/ Last updated: 2025-09-30T06:30:37.000Z [](https://medium.com/plans?dimension=post%5Faudio%5Fbutton&postId=e048e1d9220d&source=upgrade%5Fmembership---post%5Faudio%5Fbutton-----------------------------------------) ![](https://miro.medium.com/v2/resize:fit:700/1*PofLTJ-RQSaxJymmIgQHxw.jpeg) ## The Future Is Zoneless — What Can We Do Today? You’ve probably heard: Angular is moving toward a [**Zoneless**](https://angular.dev/guide/zoneless?ref=angularspace.com) future. Migrating your Angular app to run without Zone.js brings several benefits, but for medium-to-large applications, the process might not be trivial. The good news is that you can migrate an Angular application to Zoneless **gradually**. For example, migrating your components to use **OnPush** change detection is a recommended step toward Zoneless compatibility — and it also delivers **immediate performance benefits**. Nowadays, this has become much simpler when using [**Signals**](https://angular.dev/guide/signals?ref=angularspace.com). Another powerful step is **adapting your unit tests to run in Zoneless mode**, ensuring that the components under test are compatible with a Zoneless application — even **before** your app fully runs without zone.js. *While migrating unit tests will be our focus here, I recommend reading* [*this article from Angular Experts*](https://angularexperts.ch/blog/zoneless-angular?ref=angularspace.com) *for general tips on gradually adapting your application code to Zoneless.* ## Enabling Zoneless In Your Tests First of all, add [provideZonelessChangeDetection()](https://angular.dev/api/core/provideZonelessChangeDetection?ref=angularspace.com) to the list of your providers: ```typescript TestBed.configureTestingModule({ providers: [ provideZonelessChangeDetection(), // ... ] }); ``` It’s a good idea to enforce this for new component tests while gradually migrating existing ones. Depending on how your current components and tests are structured, you may encounter some failures once you enable it. ## Avoid Calling detectChanges() — especially more than once The [Angular documentation](https://angular.dev/guide/zoneless?ref=angularspace.com#using-zoneless-in-testbed) specifically recommends **against** manually calling `fixture.detectChanges()` in your test code: > To ensure tests have the most similar behavior to production code, avoid using`fixture.detectChanges()`when possible. This forces change detection to run when Angular might otherwise have not scheduled change detection. Instead, `await fixture.whenStable()` should be used: ```typescript // not recommended (still ok) it('should do something', () => { const { page } = setup(); page.triggerSomeAction(); page.fixture.detectChanges(); expect(something).toBe(true); }); // recommended it('should do something', async () => { const { page } = setup(); await page.fixture.whenStable(); expect(something).toBe(true); }); ``` I noted *“still ok”* in the comment above `detectChanges()` since: > For existing test suites, using`fixture.detectChanges()`is a common pattern and it is likely not worth the effort of converting these to`await fixture.whenStable().` `TestBed`will still enforce that the fixture's component is`OnPush`compatible and throws`ExpressionChangedAfterItHasBeenCheckedError`if it finds that template values were updated without a change notification However, I noticed that calling detectChanges() **more than once** often causes issues when used with OnPush and/or Zoneless: ```typescript // usually problematic - avoid! it('should correctly react on action 1 and action 2', () => { const { page } = setup(); page.triggerActionOne(); page.fixture.detectChanges(); // first call to detectChanges() expect(something).toBe(true); page.triggerActionTwo(); page.fixture.detectChanges(); // second call, often problematic! expect(something).toBe(true); }); // do this instead - keep each action separate! it('should correctly react on action 1', async () => { const { page } = setup(); page.triggerActionOne(); await page.fixture.whenStable(); expect(something).toBe(true); }); it('should correctly react on action 2', async () => { const { page } = setup(); page.triggerActionTwo(); await page.fixture.whenStable(); expect(something).toBe(true); }); ``` ## Get rid of fakeAsync() and tick() Yes, I was surprised too when I realized this. Unfortunately `fakeAsync()`and `tick() `rely on Zone.js and will no longer work once it’s completely removed as a dependency in your tests. As I mentioned [in another article](https://medium.com/javascript-in-plain-english/dealing-with-asynchronous-behavior-in-angular-tests-async-await-whenstable-vs-fakeasync-2999aef75fbf?ref=angularspace.com), these helpers have been extremely popular in the Angular testing world, so chances are you have been using them in your project. ```typescript // will not work without the zone.js dependency it('should do something async', fakeAsync(() => { const { page } = setup(); page.doSomething(); tick(); expect(something).toBe(true); })); ``` Note that you **can** keep using `fakeAsync()` and `tick()` even with `provideZonelessChangeDetection()` enabled as long as zone.js is included as a dependency in your unit tests. So this step can be postponed. ### Alternatives to fakeAsync() and tick() At the time of writing this article, the Angular team is working with testing library tools such as Jasmine and Jest to provide a proper alternative. They will likely update the Angular docs will new recommendations in the near future. Meanwhile, the recommended route is using `await fixture.whenStable()` instead. However, while awaiting `whenStable()` is indeed the recommended approach when it works, in my experience there are two cases where it may not be suitable: - When it’s not available, because you’re testing something other than a Component (e.g. a Service) - When, for some reason, it doesn’t actually wait for the desired action to complete To address this, I wrote a small utility called `tickAsync()`. The implementation is very basic, you can find it in the lightweight testing library [ngx-page-object-model](https://francescoborzi.github.io/ngx-page-object-model/?ref=angularspace.com), or just copy it from [here](https://github.com/FrancescoBorzi/ngx-page-object-model/blob/main/libs/ngx-page-object-model/src/lib/tick-async.ts?ref=angularspace.com). Example usage: ```typescript import { tickAsync } from 'ngx-page-object-model'; // or copy it from GitHub it('should do something async', async () => { const { page } = setup(); page.doSomething(); // use this instead of tick() whenever fixture.whenStable() cannot be used await tickAsync(); expect(something).toBe(true); }); ``` With this I was able to fix most of the tests where `whenStable()` couldn’t help. Only in a few rare cases I had to also manually wait with a delay: ```typescript // ⚠️ it will ACTUALLY wait for 100ms - not ideal. await tickAsync(100); ``` **This is however, not ideal. Tests should not await real-time.** The best alternative for fake timers are actually provided by the testing framework such as Jasmine or Jest. While we wait for an official recommendation from Angular (you can follow [this GitHub issue](https://github.com/angular/angular/issues/55295?ref=angularspace.com) for more information), it is worth [mentioning an example of using mock clocks in Jasmine](https://github.com/angular/angular/blob/3a320bdec996673abbec8646bb417bd3618030af/adev/src/app/editor/code-editor/code-mirror-editor.service.spec.ts?ref=angularspace.com#L146-L156): ```typescript it('should write the changed file content to the sandbox filesystem', () => { jasmine.clock().install(); jasmine.clock().mockDate(); const newContent = 'new content'; const nodeRuntimeSandboxSpy = spyOn(fakeNodeRuntimeSandbox, 'writeFile'); dispatchDocumentChange(newContent); jasmine.clock().tick(EDITOR_CONTENT_CHANGE_DELAY_MILLIES); expect(nodeRuntimeSandboxSpy).toHaveBeenCalledWith(service.currentFile().filename, newContent); jasmine.clock().uninstall(); }); ``` *Also worth mentioning* [*this PR*](https://github.com/jasmine/jasmine/pull/2042?ref=angularspace.com) *to the Jasmine library from* [*Andrew Scott*](https://github.com/atscott?ref=angularspace.com) *who has been heavily involved in the support for Zoneless. Special thanks to* [*Matthieu Riegler*](https://github.com/JeanMeche?ref=angularspace.com) *for suggesting these examples.* So using the mock clocks provided by the testing library is the best way to replace `tick(). `Different testing libraries such as Jest have similar APIs and describing all of them goes beyond the scope of this article. ## Remove zone.js dependency entirely from your tests Once you have reached the point where nothing in your tests rely on zone.js, you can remove it entirely as a dependency of your tests. Remove this from your `“test”` target in `project.json` (NX) or `angular.json` (Angular CLI) file: ```typescript // remove this line "polyfills": ["zone.js", "zone.js/testing"], ``` Or, if your project has a `test.ts` setup file, make sure it no longer imports zone.js: ```typescript // delete these lines import 'zone.js'; import 'zone.js/testing'; ``` ![](https://miro.medium.com/v2/resize:fit:700/1*zc3NjCxOOF7E1cMRhk-wSA.jpeg) Mount Etna — Photo by [Nienke Koedijk](https://nienkekoedijk.com/?ref=angularspace.com) ## Conclusions - Angular is going to be **Zoneless**, this will bring **several benefits** - Migrating to Zoneless is usually a **complex process** that can be done **gradually** - Using **OnPush** and **Signals** paves the way to Zoneless compatibility - **Unit Tests** can help you check whether your components work in a Zoneless app **even before you fully switch your app to Zoneless** - You can enable Zoneless mode **selectively** for individual unit tests - Avoid calling `fixture.detectChanges()` in your tests — especially multiple times. Prefer `await fixture.whenStable()` instead - Avoid using `fakeAsync()` and `tick()` as they cannot be used without Zone.js — use mock clocks instead --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/Screenshot-2024-11-13-at-15--1---1---2-.jpg) ### Building AI-powered apps with Angular and Gemini URL: https://www.angularspace.com/building-ai-powered-apps-with-angular-and-gemini/ Last updated: 2025-09-29T09:28:44.000Z Like it or not, we live in the age of AI, and it can be both exciting and frustrating. On one hand, AI can help us unlock almost unlimited capabilities for the apps we build; if in the past, tasks like image recognition or text classification could be a dealbreaker for the average developer, today, customers almost assume one would be able to pull things like this off in ridiculously short timeframes. On the other hand, however, the landscape of AI development is probably best described as "hostile". We move at incredible speeds, new models and approaches drop and then die out before we even have time to properly use them, lots of tutorials are actually disguised ways of selling us something, and lots of information is still being gatekeeped by more seasoned developers, hidden behind buzzwords like "agentic AI" , "RAG" and so on. This can be particularly harsh if you're a JavaScript developer, especially working on frontend, like me, with Angular. "Do I need to learn Python to do AI?", "I'm not a backend developer, how can I build apps with AI?", and "How do I even get started, there's so much stuff out there!" are all questions I've heard from developers in my community. Well, to be honest, those are questions I've asked myself as well. So, for the past couple of months I've been working with the Gemini API, building different apps and tools, and now is the time I dispel some myths for other Angular developers and help them begin their AI journey too. So, hereby, we begin a series of articles on how to build AI-powered apps with Angular and Gemini. This is going to be a ground-up tutorial, broken down into atomic topics that will help you get started without issue. The good news is, no prior knowledge is assumed! If you are an Angular developer who has no idea how people build apps on top of AI models, this is where you start! ## About the article series In this series, we will cover the following topics: 1. **Getting started with Gemini API**: Accessing the, API, making requests, creating chats, and the most important configuration options. 2. **Using embeddings**: Learn about how we can utilize LLMs for more than just generating text 3. **Building RAGS**: Learning about retrieval augmented generation and how to build RAG apps with Gemini and an Angular frontend 4. **Using multimodal capabilities**: How to work with images, audio, and video in Gemini 5. **Building agentic AI apps**: How to build apps that can reason, plan, and execute tasks on their own 6. **Slightly touching machine learning**: How we can actually forgo LLMs entirely and build way more reliable AI tools tailored for very specific tasks 7. **A lot more!** > Please do not assume this is going to be just a 7 article series, it is very possible I will break down the topics into way smaller chunks, so could end up with a lot more articles than just 7! So, let's start our journey into AI + Angular! ## Getting started with Gemini API Before we proceed, we must understand how exactly people build apps on top of LLMs. If you did not know that, and just assumed developers make API calls to OpenAI or Gemini or whatnot, well, you were entirely correct! (I swear I didn't generate this last phrase with AI :D). However, it is even better than that, since Gemini (and other LLM providers, but we focus only on Gemini) offer a specialized SDK that makes it incredibly easy to work with the API. To get started, let's generate a new Angular app, and install the Gemini SDK inside it. ```bash npm i @google/genai ``` This will install the Google Generative AI SDK, which we will then use to make requests to the Gemini API in a way that is better than spamming `fetch` calls. Now, before we proceed, we need to get over the first obstacles newcomers have to face, which often, in my experience, result in people being intimidated and giving up; that is getting an API token, and setting up billing, which lots of less experienced developers associate with spending money without seeing it, making them hesitant and afraid of sudden large charges. However, I have great news, since Google offers both a free tier, and some pretty decent models that are very cheap! So, let's do the following steps: 1. Go to the [Google Cloud Console](https://console.cloud.google.com/?ref=angularspace.com) and log in with your Google account. 2. Create a new project, give it a name that you will remember 3. Then head over to Google AI Studio: [https://studio.google.cloud.com/](https://studio.google.cloud.com/?ref=angularspace.com) 4. Find the "Get API key on the left sidebar" to navigate to [this page](https://aistudio.google.com/apikey?ref=angularspace.com) 5. Create a new API key associating the key with the project you created in step 2 6. Copy the API key somewhere safe, we will need it in a moment Now, since we have the API key, we might be tempted to just create an Angular service, create the API instance with our key, and start making requests. **Please do not do this!** Think about it for a moment, if we put the API key in our frontend code, anyone can just open the devtools, find the key, and start making requests on our behalf, which is both a security and monetary risk. Instead, we are going to build a small backend that will handle the AI part for us, and an Angular service that will make requests to our backend. This way, our API key is safe, and we can also implement additional logic in our backend, like caching, rate limiting, and so on. Don't be too hesitant here, we are not going to build a complex backend, but essentially a thin wrapper between our frontend and the Gemini API. We are going to use Express.js for this; if you're not familiar with it at all, keep reading the following sections; if you are you can skip to the final code example at the end of the section and continue from there. ### Building a small Express.js backend Express.js is a minimal Node.js web framework that allows us to build backend apps via Node.js. To get started, we can, in the same Angular project we just started, install express: ```bash npm install express cors ``` Then, we can create a new file called `server.js` in the root of our project, and add the following code: ```javascript const express = require('express'); // importing express const app = express(); const cors = require('cors'); // to handle CORS app.use(cors()); // enable CORS app.use(express.json()); // to parse JSON bodies app.get('/', (req, res) => { res.send('Hello!'); }); app.listen(3000, () => { console.log('Server is running on port 3000'); }); ``` Afterwards, we can open `http://localhost:3000` in our browser, and we should see "Hello!" displayed. Short recap: we created an express app, and declared a route, which, when visited via a browser or a direct API call like `fetch`, will return the text "Hello!". We also set up the app to listen on port 3000, which is where we can access it. Now, we want to actually create an endpoint that will allow us to play with the Gemini API. Before we do that, we need to figure out where to put our API key, since, again, we cannot put it into the source code itself (what if we want to push it to Github, for example?). The best way to do this is via environment variables, which could already be familiar for you even if you have been working with Angular exclusively your entire life. A popular way to do that in NodeJS is via `.env` files and using the `dotenv` package. So, let's install it: ```bash npm install dotenv ``` Then, create a new file called `.env` in the root of your project, and add the following line: ``` GEMINI_API_KEY=your_api_key_here ``` Then, we will slightly modify our `server.js` file to load the environment variables from the `.env` file: ```javascript const express = require('express'); require('dotenv').config(); // load environment variables from .env file // rest of the code stays the same for now ``` Now, to see this in action, let's modify our "Hello!: endpoint and make it actually generate some content via Gemini. First, we will import the GenAI SDK< create an instance of the API client, and then use it to generate some text: ```javascript const genAI = new GoogleGenAI({}); app.get('/', async (req, res) => { const response = await genAI.models.generateContent({ model: 'gemini-1.5-pro', // specify the model to use contents: 'Give me a random greeting', }); res.json(response); // return the response as JSON }); ``` Now, what we see here is very simple without even an explanation; we create an instance of GoogleGenAI, and then in our endpoint, we call the `generateContent` method, specifying the model we want to use (in this case, `gemini-1.5-pro`, which is a pretty capable model for most tasks), and the content we want to generate. The response from the API is then returned as JSON. One interesting thing here is that we did not specify the API key anywhere in the code. This is because the GenAI SDK automatically picks up the API key from the environment variable `GEMINI_API_KEY`, which we set in our `.env` file. This is a very convenient feature, as it allows us to keep our API key out of the source code completely. Now, let's head to `http://localhost:3000` in the browser once more to examine the response. We might see something like this: ```json app.use(express.json()); // to parse JSON bodies { "sdkHttpResponse": { "headers": { } }, "candidates": [ { "content": { "parts": [ { "text": "Howdy!\n" } ], "role": "model" }, "finishReason": "STOP", "avgLogprobs": -0.0416780412197113 } ], "modelVersion": "gemini-1.5-pro-002", "usageMetadata": { "promptTokenCount": 5, "candidatesTokenCount": 3, "totalTokenCount": 8, "promptTokensDetails": [ { "modality": "TEXT", "tokenCount": 5 } ] } } ``` There might be other fields in the response too, but we mainly care about these 3 top level fields, so let's quickly explore them: - `sdkHttpResponse`: This contains the raw HTTP response from the API, including headers and status code. This can become very useful if the model, for whatever reason, returns an error, and we want to either debug it or show a proper message to the user. - `candidates`: This is the main star of the show, as it contains the actual generated content from the model. In this case, we asked for a random greeting, and the model responded with "Howdy!". The `candidates` array can contain multiple responses, which will become important when we start streaming responses instead of picking the finalized one - `usageMetadata`: This contains information about the token usage for the request, which can be useful for monitoring and optimizing costs. We will explore this a bit later. Now, since we have our backend set up, we can now create an Angular service that will make requests to our backend instead of directly to the Gemini API. ### Creating an Angular service to interact with our backend For now, we only covered a very small portion fo what we can do with Gemini API, so let's take it a step further and define an endpoint that actually takes some input from the user and responds to it, instead of just generating a greeting: ```javascript app.post('/generate', async (req, res) => { const { prompt } = req.body; // get the prompt from the request body if (!prompt) { return res.status(400).json({ error: 'Prompt is required' }); } try { const response = await genAI.models.generateContent({ model: 'gemini-1.5-pro', contents: prompt, }); res.json(response); } catch (error) { console.error('Error generating content:', error); res.status(500).json({ error: 'Failed to generate content' }); } }); ``` While this a bit more code than we had previously, it does not do anything complex, and simply takes a `prompt` from the request body, and uses *it* to generate content via Gemini. If the prompt is missing, it returns a 400 error, and if there's any error during the generation, it returns a 500 error, pretty standard stuff. Now, we can go ahead and create an Angular service that will make requests to this endpoint. ```typescript export type GeminiResponse = { candidates: { content: { parts: { text: string }[] } }[]; } @Injectable({providedIn: 'root'}) export class GenAIService { readonly #http = inject(HttpClient); generateContent(prompt: string) { return this.#http.post('http://localhost:3000/generate', {prompt}).pipe( // map the response to just return the generated text map( response => response.candidates[0]?.content.parts[0].text || 'No response', ) ) } } ``` As we can see, the `GeminiResponse` type we created is a quite intimidating on its own, even more so considering we omitted most of the fields, leaving behind only the part that actually contains the generated text. However, don't be scared by it, simply copy it and keep, since 90% of the time you will only care about the inner `parts` field that contains the actual data. Now, we can set up a component to use this service and display the generated content: ```typescript @Component({ template: `

AI Text Generator

@let response = generatedResponse();

Response:

{{ response.text }}
`, }) export class GenerateTextComponent { readonly #genAI = inject(GenAIService); prompt = signal(''); generatedResponse = signal<{text: string, error: string | null}>({ text: '', error: null, }); generateResponse() { // I would be very very happy to do this via resources // but they do not yet support POST requests // P.S. read more about resources in my article: https://www.angularspace.com/meet-http-resource/ this.#genAI.generateContent(this.prompt()).subscribe({ next: (response) => this.generatedResponse.set({ text: response, error: null, }), error: () => this.generatedResponse.set({ text: '', error: 'Error generating text', }) }); } } ``` As we can see, on the frontend part this is reasonably simple, as we just invoke our service, make the HTTP request, store the data in a signal, and display it. Nothing too fancy, and nothing too complex. At this point, we might want to get excited and jump into more complex stuff, like making chats, streaming responses and so on, however, I suggest we make a sideways move and explore a little bit some of the configuration options we have when making requests to Gemini, since this will help us a lot in the future. ## Configuring Gemini API ### Configuring models Let's go back for a moment, and remember that we selected a specific model to make requests to, namely `gemini-1.5-pro`. While this is a very capable model by itself, we might want to explore other models, since different tasks might require using more (or sometimes, surprisingly, less!) powerful models. We can do this by creating an endpoint that specifically lists the available models: ```javascript app.get('/models', async (req, res) => { try { const response = await genAI.models.list(); res.json(response); } catch (error) { console.error('Error listing models:', error); res.status(500).json({ error: 'Failed to list models' }); } }); ``` Now, we can create a component with an `httpResource` that allows us to see the list of models: ```typescript @Component({ template: `

Available Models

@if (modelsResource.isLoading()) {
Loading models...
} @if (modelsResource.error()) {
Error loading models: {{ modelsResource.error() }}
} @if (modelsResource.value(); as models) {
    @for (model of modelsResource.value()?.pageInternal; track model.name) {
  • {{ model.displayName }}

    Name: {{ model.name }}

  • }
}
`, }) export class ModelsListComponent { modelsResource = httpResource<{pageInternal: {name: string, displayName: string}[]}>( () => 'http://localhost:3000/models' ); } ``` Now, if we open this component, we will see a big list (around 50) of Gemini models, each tailored for different tasks. Some models are better at reasoning, useful for tasks involving complex instructions and logic, some are simply better as conversational and work faster, some are specialized for image or video generation, and some are embedding models (we will learn about those in later articles of this series). Choosing a model is a challenging task, and often requires some trial and errors, but in general it's important to keep in mind that choosing a model usually comes down to balancing the following three factors: 1. Cost of the model: more capable ones are usually more expensive 2. Speed of generation: models that have reasoning capabilities are usually slower unless we disable the thinking mode, but can be better for solving tasks instead of just text generation 3. The task at hand we're trying to solve will influence the model choice, very often we do not need to latest and shiniest models, just a simple one that can do the job decently, saving us money and the user's time You can read way more about the models, their capabilities and pricing in the [official documentation](https://ai.google.dev/gemini-api/docs/pricing?ref=angularspace.com). Now, let's take a look at what actually goes into pricing the model usage ### How much will you spend Let's go back to our very first example, and take a look at the `usageMetadata` field in the response: ```json { "usageMetadata": { "promptTokenCount": 5, "candidatesTokenCount": 3, "totalTokenCount": 8, "promptTokensDetails": [ { "modality": "TEXT", "tokenCount": 5 } ] } } ``` As we can see, the `usageMetadata` field contains information about the token usage for the request. If you're unaware what "tokens" are in the context of LLMs, keep reading, but if you know what that is, feel free to skip the next 3 paragraphs. To understand what tokens are, we need to understand (not deeply, at a very high level), how LLMs work. A large language model generates text by predicting the next "token" in a series of tokens. A token can be a word, a part of a word, a punctuation mark, or a special symbol. To quickly and visually understand what tokens are, we can use the [OpenAI tokenizer tool](https://platform.openai.com/tokenizer?ref=angularspace.com). For example, if we input the text "Hello, world!", we will see that it is broken down into 4 tokens: "Hello", ",", " world", and "!". LLMs work by taking your text, breaking it down into tokens, and then predicting the next token based on the previous ones. This is how they generate coherent and relevant text. It is important to know that tokens are essentially fixed, so in different contexts, the same text will always be broken down into the same tokens. Understanding what tokens are, we can now understand how Gemini API pricing works. The amount you will be charged for using a model is based on the number of tokens processed during your requests. This includes both the tokens in your input (the prompt you send to the model) and the tokens in the output (the text generated by the model). If we revisit the Gemini API models list page, we will see that each model has a different price for input and output tokens (with output tokens usually being more expensive). While the prices might seem intimidating at first, it's important to take note that those are prices for generating 1 million (!) tokens, which, for our local, learning-oriented app is simply a grotesquely big amount. It is roughly equal to 750,000 words, which is double the word count of the entire "Lord of the Rings" trilogy! So, for most learning and prototyping purposes, you will be spending just a few cents, if anything at all (and that is if you go out of the free tier limits). Now, as we gathered an understanding of how the API pricing works, let;s finally explore some parameters we can use to modify the responses we get from an LLM at a very high level before finalizing the first part of this series. ### LLM response configuration Congratulations, you have arrived at the buzzword section of this article! Here, we will explore some of the most important options LLMs can take, of which you have undoubtedly heard of, even if maybe not comprehending them fully. These are `temperature`, `topP`, `topK`, `maxOutputTokens`. Here's a high level overview: - `temperature`: This parameter controls the randomness of the model's output. Previously, we explained that LLMs generate text by predicting the next token based on the previous ones. This was a bit of an oversimplification; LLMs don't come up and say "the next token is `cat`!". Instead, they generate probabilities for each possible token first, so, for instance, they may say something like "there's a 30% chance the next token is `cat`, a 25% chance it's `dog`, a 15% chance it's `fish`, and so on". And usually, they do **not** simply pick the most probable token. Think about it, if they simply always chose the most probable token, they would become robotic and repetitive, which is not what is usually desired. Instead, they often try to pick some tokens that might be less probable, but still make sense in the context. This is where `temperature` comes into play; it controls how much randomness we want in the token selection process. A low temperature (e.g., 0.2) makes the model pick the highest probable words more often, resulting in a more predictable text, while a higher temperature (e.g., 0.8) makes the model pick less probable words more often, resulting in more creative text. > Note: `temperature` is not some hard science setting; you might have heard somewhere that a temperature of "0" makes the model "deterministic" (oh boy do AI folks love buzzwords), but in reality, it is still not what we would mean by that words in a usual, literary context, since a slight variation in the input prompt (like a missing comma) might still result in a vastly different LLM output. Temperature can be useful from time to time, depending on the task, but is not a silver bullet to fix your LLM's output - `topK`: This parameter is used to make a hard limit for choosing the next token; if temperature allowed us to pick less probable tokens out of **all** possible tokens, `topK` actually just limits the set of tokens to choose from, so if we set the limit to, say, 3, the model will only consider the 3 most probable tokens when picking the next one. To be honest, most people usually do not bother with this, since it is a quite blunt tool and there is no way to predict if actually the 3 (or 7, or 12) tokens are actually the relevant ones, or if some lower-valued token is actually better - `topP`: This parameter is a bit more complex and more useful than `topK`. Instead of limiting the number of tokens to choose from, it limits the cumulative probability of the tokens to choose from. "Cumulative" here simply means "adding up the probabilities until we reach a certain threshold". For example, if we set `topP` to 0.9, the model will consider the most probable tokens until their combined probability reaches 90%. This allows for a more dynamic selection of tokens, as the number of tokens considered can vary based on their probabilities. - `maxOutputTokens`: This parameter simply limits the maximum number of tokens the model can generate in its response. This is useful to prevent the model from generating excessively long responses, which can be costly and time-consuming. For example, if we set `maxOutputTokens` to 50, the model will stop generating text after producing 50 tokens. > **Important**: the `maxOutputTokens` just bluntly limits the amount of generated tokens, it does **not** make the model generate shorter text. It is supposed to be a more of a safety net to ensure your LLM does not go ballistic and cost you a fortune. For generating shorter content, you should look into writing prompts that guide the model to try to be shorter (again, not an exact science, but usually works decently) All the parameters we mentioned are available in the Gemini API SDK, and we can now modify our text-generation endpoint to accept them as well: ```javascript const response = await genAI.models.generateContent({ model: 'gemini-1.5-pro', contents: prompt, config: { topP: 0.5, temperature: 0.1, maxOutputTokens: 50, } }); ``` If we now retry the same messages we put into our Angular app's generate page, we might see more coherent and fixed responses, and, since `maxOutputTokens` is set to such a low value, some messages might be cut-off. ## Conclusion Wow, I bet this was a lot of information. However, this article might also leave you feeling that we have just scratched the surface (which is true). So, let's recap - We learned how LLMs generate text, learned about tokens, configurations parameters like temperature, `topP` and so on - We learned how to create a Google Cloud project, get a Gemini API key, and use the SDK to make text generation requests - We learned about the diverse set of models we can utilize in the future for different tasks - We did all of this while requiring minimally more knowledge than any Angular developer possesses If this was exciting, wait until you hear about the next article! In the second one, we are going to - Learn how to stream responses to make up for a better UX - Learn how to create chats, and maintain context between messages - Learn a bit of prompting to secure better responses from the model - Touch on structured outputs which can help us solve more direct tasks than just text generation I hope you enjoyed this article, and see you in the next one! ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) P.S There is one chapter in my book that helps you work LLMs in the context of Angular apps; that chapter is already kind of outdated, despite the book being published just earlier this year (see how insanely fast-paced the AI landscape is?!). I hope you can forgive me ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/Screenshot-2024-11-13-at-15.52.00--1--1--2-.jpg) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/angular-university-banner-4--3--1.jpg)](https://angular-university.io/?ref=angularspace.com) ### afterRenderEffect, afterNextRender, afterEveryRender & Renderer2 URL: https://www.angularspace.com/afterrendereffect-afternextrender-aftereveryrender-renderer2/ Last updated: 2025-09-16T11:29:46.000Z Recently I’ve been playing around with some Angular functionalities, which are: `effect`, `afterRenderEffect`, `afterNextRender`, `afterEveryRender` and `Renderer2`. You don’t see them used much compared to signals or computed. Maybe only `effect` is more common, however how and when to use the rest? I wanted to write about them because I kept mixing them up myself, therefore this post is as much for me as for anyone else. I want to touch on each of them, look at some examples, how they differ, and also check how they behave with SSR. ## Using `effect` [The effect()](https://angular.dev/guide/signals?ref=angularspace.com#reading-without-tracking-dependencies) will run at least once and then every time the dependency signal (or multiple one) changes. A shameless plug to my [Senior Angular Interview Questions](https://www.angularspace.com/senior-angular-interview-questions/), I talked about the diamond problem in RxJS and how it’s not present in signals. Meaning if you have an effect with multiple signal dependencies and you update those signal, one after another, then the `effect` will still run only once, since signals are synchronous compared to Observables which are asynchronous and the logic could be re-executed multiple times, causing side-effects. Use `effect` when you want to bridge the gap between a reactive state (signals) and a non-reactive execution. Use cases which you hear many times are mainly DOM updates, logging, or even executing a `fetch()` API call to the server. Other less common examples may be for example local storage updates, analytics tracking, chart data updates or setting loading state. ```typescript // state of the used theme readonly theme = signal<'light' | 'dark'>('light'); // track what page we are on readonly currentPage = signal('home'); // data to render a chart readonly chartData = signal([1, 2, 3]); // loading state of the app readonly loading = signal(false); constructor() { effect(() => { // change theme & save it document.body.dataset.theme = this.theme(); localStorage.setItem('theme', this.theme()); }); effect(() => { // sends data to a 3rd party analytics.trackPage(this.currentPage()); }); effect(() => { // updates values in the chart updateChart(this.chartData()); }); effect(() => { // dependency to listen to const chartData = this.chartData(); untracked(() => { this.loading.set(true); }) }) } ``` When it comes to SSR, you have to be a bit careful with `effect()`. It also runs on the server, at least once (even if dependencies are `undefined`), and then all the time when its dependencies change. An empty effect is also executed: `effect(() => console.log('Empty effect'));` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/empty-effect.png) Empty Effect Execution In SSR you don’t actually have a browser, so there’s no `window`, `document`, or localStorage. If you drop DOM calls or browser APIs directly into an effect, it’ll throw during the server render. The trick is to only use `effect()` on the server for things that are safe in a Node environment. Anything that touches the DOM should be wrapped in something like `isPlatformBrowser` or pushed into `afterRenderEffect`, which runs only in the browser after Angular has finished painting. Another point worth highlighting is cleanup function - [EffectCleanupRegisterFn](https://angular.dev/api/core/EffectCleanupFn?ref=angularspace.com). Just like in RxJS where you unsubscribe from a stream, `effect()` also gives you a way to tear things down when the effect is destroyed. This is helpful when wiring up elements like event listeners, intervals, or external libraries that require explicit cleanup. It's the first available argument in the `effect()` function. There is no naming convention, it will work with any name, but most developers call it `onCleanup()`. Use it inside your effect to make sure you don’t leak memory or leave hanging listeners when the component goes away. It’s easy to forget, but in larger apps, this can save you from nasty performance issues. ```typescript @Component({ selector: 'app-resize-listener', template: `

Window width: {{ width() }}

` }) export class ResizeListenerComponent { // reactive signal that stores the current width readonly width = signal(window.innerWidth); constructor() { effect((onCleanup) => { const updateWidth = () => this.width.set(window.innerWidth); window.addEventListener('resize', updateWidth); // cleanup when effect is destroyed onCleanup(() => { window.removeEventListener('resize', updateWidth); }); }); } } ``` ## Using `afterRenderEffect` The `afterRenderEffect` is more preferable for DOM updates, browser APIs, or integrations that don’t make sense on the server (like canvas drawing, chart rendering, or measuring element sizes). It is executed after Angular has painted the view in the browser. From the first examples, you could say that updating data in charts is better suited for the `afterRenderEffect` since we are performing a DOM update. When you open up the [documentation](https://angular.dev/api/core/afterRenderEffect?ref=angularspace.com) for this function, Angular team highlights that “*You should prefer specifying an explicit phase for the effect instead, or you risk significant performance degradation.*” In real life, you may have an example of displaying a PDF file to an user and you want to track (in percentage) how far he has scrolled in the document. One of the (many) ways how to achieve this behavior is the following: ```typescript @Component({ selector: 'app-root', template: `
`, }) export class App { readonly divRef = viewChild>('divRef'); readonly divTop = viewChild>('divTop'); readonly scrollPercentage = toSignal( toObservable(this.divRef).pipe( filter((el) => !!el), switchMap((el) => fromEvent(el.nativeElement, 'scroll').pipe( map(() => { const scrollHeight = divRef.nativeElement.scrollHeight ?? 1; const clientHeight = divRef.nativeElement.clientHeight ?? 1; const scrollTop = divRef.nativeElement.scrollTop ?? 0; const scrolled = Math.round( (scrollTop / (scrollHeight - clientHeight)) * 100 ); return scrolled; }) ) ) ), { initialValue: 0 } ); constructor() { afterRenderEffect({ // creating dependency on the scroll signal earlyRead: () => this.scrollPercentage(), // write to DOM every time scrollPercentage emits write: (val, cleanUp) => { const divTop = this.divTop(); if (!divTop) { return; } divTop.nativeElement.innerText = `Scroll: ${val()}%`; }, }); } } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/scroll-counter.gif) Scroll Attached Using afterRenderEffect In the above example, the `afterRenderEffect` is triggered once the browser finishes painting the DOM element. It then uses the `earlyRead` callback to register the `scrollPercentage` signal dependency. Every time `scrollPercentage` emits (as you scroll), the `write` phase is triggered to update the DOM. Of course you can achieve this exact result without using the `afterRenderEffect` and just directly interpolating the `scrollPercentage` signal in the HTML (`{{ scrollPercentage() }}`). The documentation about `afterRenderEffect` also talks about the `read` optionality, so what’s the difference? - Use `earlyRead` when you want to read the DOM before any writes happen. Angular runs the `earlyRead` callback first, so you can grab measurements (width, height, etc.), before any DOM updates, and pass that information to the `write` phase. - Use `read` when Angular has applied all styles in the `write` phase, as this one runs after it. It’s good when you want to read some correct measurements after all UI changes, but you can not pass those values back to the `write` phase. Only `earlyRead` allows passing values to the `write` operation. You also have the option to use the `mixedReadWrite` phase, which allows you to read and subsequently write data to the DOM; however, Angular recommends avoiding it and using the previously described ones. The phase order: 1. `earlyRead` 2. `write` 3. `mixedReadWrite` 4. `read`. One other great resource I’ve found is from Code Shots With Profanis - [Get to Know the AfterRenderEffect hook in Angular](https://www.youtube.com/watch?v=W7iOgNthNW8&ref=angularspace.com). I do recommend checking out his explanation on this topic. From my understanding, when you only use client-side rendering and you ignore the phases in `afterRenderEffect`, then it behaves the same as `effect`. My above example with the scroll can also be achieved by the following: ```typescript constructor() { // example 1 afterRenderEffect(() => { const val = this.scrollPercentage(); const divTop = this.divTop(); divTop.nativeElement.innerText = `Scroll: ${val}%`; }); // example 2 effect(() => { const val = this.scrollPercentage(); const divTop = this.divTop(); divTop.nativeElement.innerText = `Scroll: ${val}%`; }); } ``` > ***NOTE:*** However, not utilizing the rendering phases, you are risking layout thrashing. It happens when the browser is forced to repeatedly recalculate the layout, because your code is reading from and writing to the DOM in an uncoordinated way, creating a so called loop. By separating earlyRead (all reads) from write (all writes), Angular batches DOM reads before any writes happen. This avoids any unexpected looping behavior. ## Using `afterNextRender` and `afterEveryRender` The [documentation](https://angular.dev/guide/components/lifecycle?ref=angularspace.com#aftereveryrender-and-afternextrender) about these two function says that: “*we can register a render callback to be invoked after Angular has finished rendering all components on the page into the DOM.*”. The idea is the following: - `afterNextRender` runs only once after Angular paints the view for the first time - `afterEveryRender` runs after every render cycle, like a subscription to render events If we were to compare these two functions to a life cycle hook, the closest (in behavior) we would get is `afterNextRender` to `ngAfterViewInit` and `afterEveryRender` to `ngAfterViewChecked`, as `afterEveryRender` runs every time a `tick()` re-renders something dirty. Compared to life cycle hooks, `afterNextRender` and `afterEveryRender` only run on the client side, whereas life cycle hooks are also triggered on SSR. The other difference is that the hooks are scoped at the component level, while afterNextRender and afterEveryRender are scoped to the rendering of the whole app (the page we are looking at). Angular’s team also provides a simple diagram to better understand the execution order. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/angular-initialization.png) Angular Initialization Understanding `afterNextRender` is a bit simpler, since it is executed only once, after DOM has been painted. You can put logic from `ngOnInit` into it, as `afterNextRender` is called inside the `constructor`. You can render data into charts once, or focus on an empty input element: ```typescript @Component({ selector: 'app-root', template: ` `, }) export class App { readonly inputs = viewChildren>('input'); constructor() { afterNextRender(() => { const inputs = this.inputs(); const firstEmpty = inputs.find((d) => d.nativeElement.value == ''); // this will focus on the 'second' input firstEmpty?.nativeElement?.focus(); }); } } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/input-focus.png) Empty Input Focus Using `afterEveryRender` is, at least for my understanding, used for less common use cases. The question is: when do we actually need to execute some code after every rendering cycle? One example is the' onStable' method of [ZoneJS](https://angular.dev/api/core/NgZone?ref=angularspace.com). The `onStable` method is triggered every time a change detection occurs, and as we move toward zoneless applications, you can copy `onStable` logic into `afterEveryRender`, which will result in the same behavior. See the following code and GIF for a demonstration. ```typescript @Component({ selector: 'app-resize-listener', standalone: true, template: `

Text: {{ text() }}

`, }) export class ResizeListenerComponent { private readonly ngZone = inject(NgZone); readonly text = signal(''); constructor() { this.ngZone.onStable .asObservable() .subscribe((e) => console.log('ZoneJs - triggered')); afterEveryRender(() => { console.log('afterEveryRender - triggered'); }); } onClick1() {} onClick2() { this.text.update((prev) => `${prev}K`); } } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/zone-vs-afterEveryRender.gif) NgZone vs AfterEveryRender In theory I could rewrite my first example with scroll value from Observables into `afterEveryRender` and it would work the same way: ```typescript @Component({ selector: 'app-root', template: `
`, }) export class App { readonly divTop = viewChild>('divTop'); readonly divRef = viewChild>('divRef'); constructor() { afterEveryRender({ earlyRead: () => ({ divRef: this.divRef(), divTop: this.divTop(), }), write: (elements) => { const { divRef, divTop } = elements; if (!divTop || !divRef) { return; } const scrollHeight = divRef.nativeElement.scrollHeight ?? 1; const clientHeight = divRef.nativeElement.clientHeight ?? 1; const scrollTop = divRef.nativeElement.scrollTop ?? 0; const scrolled = Math.round( (scrollTop / (scrollHeight - clientHeight)) * 100 ); divTop.nativeElement.innerText = `Scroll: ${scrolled}%`; }, }); } } ``` One question I was wondering about is whether to use `afterEveryRender` or maybe reach out to `Renderer2` and apply listeners? The example with scroll percentage could be adjusted to use `Renderer2` as follows: ```typescript @Component({ selector: 'app-scroll-tracker', standalone: true, template: `

Scroll: {{ scrollPercent() }}%

`, }) export class ScrollTrackerComponent { private readonly renderer = inject(Renderer2); private readonly destroyRef = inject(DestroyRef); readonly divRef = viewChild>('divRef'); readonly scrollPercent = signal(0); // reference to the listener to destroy it with the component private removeListener?: () => void; constructor() { afterNextRender({ earlyRead: () => this.divRef()?.nativeElement, write: (box) => { if (!box) { return; } this.removeListener = this.renderer.listen(box, 'scroll', () => { const percent = Math.round( (box.scrollTop / (box.scrollHeight - box.clientHeight)) * 100 ); this.scrollPercent.set(percent); }); }, }); // destroy listener with the component this.destroyRef.onDestroy(() => { this.removeListener?.(); }); } } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/scroll-renderer.gif) Scroll Counter Attached Using Renderer2 Seems like there is always more than one solution for a problem. My understanding on the difference between these two is that: - `Renderer2` is used when we want to attach some listeners or render on the DOM - `afterEveryRender` is used when we want to fire a callback once DOM painting is done When is comes to the scroll example, the `Renderer` example makes more sense, since we track the scroll position (an event based behavior), however if we were want to scroll to the bottom of the page every time a new information is presented on the screen, in that case, `afterEveryRender` would be a more preferable option. ## Summary All in all, these tools don’t replace each other, but complement different needs. If your logic is purely reactive state, reach for `effect`. If it depends on the DOM, use `afterRenderEffect`. For one-time DOM adjustments after the first paint, use `afterNextRender`. If your logic needs to be executed on every DOM re-render, use `afterEveryRender`. Finally, `Renderer2` is a universal way to attach listeners and manipulate the DOM without risking SSR crashes. Hope you liked the article and I was able to provide some explanation on these functions. They were causing some confusion. at least for me, hence I decided to go deeper with them and try to explain them with my own words. Feel free to share your thoughts, catch more of my articles on [dev.to](https://dev.to/krivanek06?ref=angularspace.com), connect with me on [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com) or check my [Personal Website](https://eduardkrivanek.com/?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/09/Screenshot-2024-11-13-at-15--1---1---1---2-.jpg) ### Migrating to Angular Signals URL: https://www.angularspace.com/migrating-to-angular-signals/ Last updated: 2025-08-27T10:44:42.000Z Today, everyone, everywhere in the Angular community is talking about Signals. They are the new way to manage state and reactivity in Angular applications, promising a better way to build logic and a very much improved change detection. However, many are hesitant. And this is understandable! Not only are signals new, but they also require a somewhat different way of thinking as opposed to the common way of dealing with reactivity (without RxJS). And even with RxJS, many question need our attention before we cna actually move in and start using signals in our day-to-day operations. So, what are those questions that we aim to answer with this article? Let's take a look 1. Are signals going to replace RxJS? Is RxJS "dead"? 2. Should I migrate to signals? What are the benefits? 3. If so, how should I migrate? This feels overwhelming! 4. If I do, when should I use signals and when RxJS? Let's go one by one and try to answer these questions, and, hopefully, learn something new along the way. ## Are signals going to replace RxJS? This is probably the most common question everyone is asking nowadays. WHich is understandable, given both approaches kind of solve the same problems (state management and reactivity) and are both used in Angular applications. To answer this, let us begin with the official position of the Angular team: RxJS is going to be *optional*. Now, optional is a specific word. It means it will **not** be *required*, but it will definitely be *supported*. So what does that mean in practice? Well, materially, the actual support for RxJS improved with the latest version. For example, we go the `takeUntilDestroyed` custom operator to help us unsubscribe easier from our Observables. On top of that, we now have the `@angular/core/rxjs-interop` package that also allows us to play signals together with RxJS. So, the correct answer here is no, RxJS is not going to die. Moreover, this year a big step has been taken to make Observables native in browsers: Chromium-based browsers now support an experimental, [native version of Observables](https://medium.com/ng-news/ng-news-25-15-native-observables-4784d026e136?ref=angularspace.com). This only can mean that in the future reactive extensions (even if not in the form of RxJS) will become *more* ubiquitous, not less. So, this is more or less the answer to our first question: no, RxJS is not dead, and is not planning to become dead anytime soon, and when it comes to Angular, the support for it has improved, it is just that it will be optional and many tasks can be completed with signals instead. Which naturally brings us to the next question. ## Should I migrate to signals? Now this one is a bit tricky. Signals are way simpler as opposed to RxJS, but also have a learning curve, so we must justify moving away (in some cases!) from RxJS. Let's shortly recap what benefits signals bring to the table. 1. Synchronous: Observables can be asynchronous, which can add to the overall confusion. Signals, on the other hand, are guaranteed to be synchronous, meaning no race conditions and other unpleasant surprises. (of course this also means they are unsuited for dealing with async things directly, but we are listing benefits here don't we?) 2. Always available value: with Observables, we need to subscribe in order to be able to read the latest "value", we can always simply call them (`mySignal()`) and get the current value immediately. 3. Simplicity: In RxJS, we have multiple powerful operators and other concepts such Schedulers, hot and cold Observables, Subjects, etc. Signals have a very thin API layer, providing the bare minimum building blocks to handle everything we need in terms of 4. Built for Angular: while we discussed the improved RxJS interoperability in Angular, signals are built specifically for Angular, meaning they are designed to work with the framework's change detection seamlessly. Updating a signal's value already triggers change detection, making them essential for [Zoneless Angular apps](https://angular.dev/guide/zoneless?ref=angularspace.com). Now, if this got us convinced that we want, in fact, to migrate to signals, we can move to the next question: how do we do that? ## How should I migrate to signals? To correctly address this question, we need to first understand that this migration won't happen overnight (unless we have a really tiny app). However, we also can realize that changing one component property from an Observable (or even better, a conventional property like a `string` or `boolean`) to a signal is not that hard, and, crucially, most likely won't affect the overall component logic. This gives us the understanding that we can start migrating to signals in an incremental, one-by-one fashion. Let's now outline the important steps and then deconstruct all of those steps further. 1. Easy steps: migrating input/output properties and view/content queries 2. Migrating simple properties: primitive values can be dealt with here and there 3. Migrating object properties: this is where things can get a bit more complicated, but still manageable 4. Migrating `BehaviorSubjects`: also can be done easily, but a degree of care should be shown 5. Migrating other Observables: really depends on the case, and mostly involves removing `async` pipes in favor of `toSignal`, rather than actually changing the Observable in question to a signal So, let's discuss all these points! ### Migrating inputs/outputs and view/content queries As we known, in recent versions, Angular has introduced the `input` and `output` functions, which replace the previous `@Input` and `@Output` decorators. The `input` function in particular produces a signal, so it can be used with all sorts of building blocks like `computed`, `effect` and now also `linkedSignal`. So, for instance, if we have such a component: ```typescript @Component({ selector: 'my-dialog', template: `...`, }) export class MyDialogComponent { @Input() open: boolean; @Output() close = new EventEmitter(); } ``` We can now safely migrate it to signals like this: ```typescript @Component({ selector: 'my-dialog', template: `...`, }) export class MyDialogComponent { open = input.required(); close = output(); } ``` Now, we are not going to dive into all the intricacies of having inputs and outputs as signals, so you can consult the official documentation [here](https://angular.dev/guide/components/inputs?ref=angularspace.com) and [here](http://angular.dev/guide/components/outputs?ref=angularspace.com) for that. However, what actually concerns us, is the big scale of such a migration: in a decent enterprise-grade app we might very well have hundreds of component with thousands of inputs that would be almost impossible to convert manually. Thankfully, the Angular team got us covered (as much as possible), with a special migration schematic we can run and easily convert most (if not all) of our inputs and outputs. We can simply run ```bash ng generate @angular/core:signal-input-migration ``` And all of the inputs/outputs that are safe to convert will be automatically converted, and if they are referenced somewhere, the reference would be updated to actually call the signal (reading its value), even in templates and host bindings. Now let us discuss what "safe to convert" means. For instance, the schematic might not convert inputs that are being re-set in the component, as signal inputs are immutable, so we cannot change their value after the initial assignment, only parent components can. This is a good thing, as it helps us avoid bugs, but it also means that we need to be careful when migrating such inputs: ```typescript @Component({ selector: 'my-dialog', template: `...`, }) export class MyDialogComponent { @Input() open: boolean; @Output() close = new EventEmitter(); closeDialog() { this.open = false; // this will not work if input is a signal this.close.emit(); } } ``` There's a way to force the schematic to do a bit more and convert slightly unsafe inputs: ```bash ng generate @angular/core:signal-input-migration --best-effort-mode ``` However, you should be careful with this, as it could break your build, so always double-check. Now, after doing the migration, we need a way to circle back later and manually migrate inputs that were not converted. For this, when running the migration, we can use an `--insert-todos` flag, which will insert `TODO` comments in the code where manual migration is required. This way, we can easily find those places later and fix them. For example: ```typescript @Component({ selector: 'my-dialog', template: `...`, }) export class MyDialogComponent { // TODO: Skipped for migration because: // Your application code writes to the input. This prevents migration. @Input() open: boolean; close = output(); closeDialog() { this.open = false; // this prevented it this.close.emit(); } } ``` Another small case could be if we use getters as means to read inputs and do some side-effects or convert to another value. Here, we would be forced to also come in manually and change those to either be an `effect` or computed\` instead in the future. Next, if we have a truly huge application we might consider doing even the automatic migration incrementally; for this, we cna provide a path to the schematic and only convert a part of our app at a time, test it, ship it, then come back for the next slice: ```bash ng generate @angular/core:signal-input-migration --path=src/app/some/feature ``` It is worth noting, that even the manual migration does not have to be painful: there's a VSCode [code refactor action](https://code.visualstudio.com/docs/typescript/typescript-refactoring?ref=angularspace.com#%5Frefactoring) available that can convert an `@Input` or `@Output` to a signal, after which we can manually hone any remaining edge cases. ![How the refactor works](https://angular.dev/assets/images/migrations/signal-inputs-vscode.png) Everything we mentioned here is actually the worst case scenario; we can run a migration for outputs separately, which is way safer: ```bash ng generate @angular/core:signal-output-migration ``` Since it is safe, it does not include a `--best-effort-mode` flag, and it does not insert `TODO`s, as it is guaranteed to work. It also handles cases where the legacy `EventEmitter`'s `next` method is used, and removes calls to `EventEmitter.complete`, since it is unnecessary. Finally, we must say that we can also run a migration schematic to change view and content queries to signals, which might also be unsafe, so `--best-effort-mode` is available here as well: ```bash ng generate @angular/core:signal-queries-migration ``` This will do! When we have our inputs, outputs and queries migrated, we can move to the next step. ### Migrating simple properties Now, when we have inputs and outputs and other things that might naturally be signals, we now arrive at a territory where we need to go manual, however still keeping things relatively simple. Imagine we have a property in our component that is a simple primitive value, like a `string` or `boolean`. We can simply change it to a signal, and then use it as such: ```typescript @Component({ selector: 'some-component', template: ` `, }) export class MyComponent { open = false; toggleDialog() { this.open = !this.open; } } ``` Now, such a signal can easily be converted to a signal, and we can use it in the template as well: ```typescript @Component({ selector: 'some-component', template: ` `, }) export class MyComponent { open = signal(false); toggleDialog() { this.open.update((value) => !value); } } ``` This is very straightforward! Basically, we have to keep several simple things in mind: 1. Change a property to be a signal of a value instead of the value itself ('true' -> 'signal(true)') 2. Change update logic from simple assignment to using the `update` or `set` methods of the signal: - `this.open = !this.open` \-> `this.open.update((value) => !value)` 3. Do not forget to call the signal in the template as a function: `open` \-> `open()` 4. If your signal is bound to `[(ngModel)]`, do **not** call it in the binding, just use it as is: `[(ngModel)]="open"` \-> `[(ngModel)]="open"` This was easy. Now, doing the same with complex values (like deeply nested objects and arrays) can be a bit trickier. ### Migrating complex properties Now, with complex properties as signals, the manual steps are still the same, we just keep in mind that usually we would mostly use `.update` instead of `.set` (although not exclusively), as it is convenient when dealing with properties, like adding an item to an array: ```typescript arraySignal.update((array) => [...array, newItem]); ``` Now, what can become frustrating here is using these methods with objects of considerable size and depth. Imagine we have a `state` signal that contains various items, one of which is a `product` object, which has a `orderHistory` array, which contains `order` objects, which have a `quantity` property. Now imagine updating that property for the third `order`: ```typescript state.update((state) => { return ({ ...state, product: { ...state.product, orderHistory: state.product.orderHistory.map((order, index) => { if (index === 2) { // third order return { ...order, quantity: order.quantity + 1 // increment quantity }; } return order; // return other orders unchanged }), } }) }); ``` Looks ugly and confusing. Angular itself does not provide a way to do this, but we can use a library like [immer](https://immerjs.github.io/immer/?ref=angularspace.com) to help us with this. Immer simplifies working with immutable data structures by letting us write code that looks like we're directly modifying an object, array, etc, but behind the scenes, it takes these "mutations" and efficiently produces a brand-new, immutable version of our data without affecting the original object. This means we get the benefits of immutability, such as a more predictable state, without the boilerplate of manually copying and updating nested data structures. It allows us to simple do the following: ```typescript import { produce } from 'immer'; state.update((state) => { return produce(state, (draft) => { draft.product.orderHistory[2].quantity += 1; // increment quantity }); }); ``` This quickly becomes very reasonable. Another concern with more complicated properties is that they might be implicitly tied to other properties without that process being handled reactively. For instance, consider the following code: ```typescript @Component({ selector: 'some-component', template: ` `, }) export class SomeComponent implements OnChanges { @Input({required: true}) options: {id: number, name: string}[]; selectedOption = this.options[0]; // default selection ngOnChanges(changes: SimpleChanges) { if (changes['options']) { this.selectedOption = this.options[0]; // reset selection if options change } } } ``` Now, we can see that the `selectedOption` is implicitly tied to the `options` input, and if we change the options, we need to reset the selection. This is achieved in a bit of an ugly way here, using the `ngOnChanges` lifecycle hook as an intermediary between a dependent state and its source value. With signals, if we convert the `options` input to an input signal, we can use `linkedSignal` to reset the option in a reactive manner: ```typescript @Component({ selector: 'some-component', template: ` `, }) export class SomeComponent { options = input.required<{id: number, name: string}[]>(); selectedOption = linkedSignal({ source: this.options, computation: (options) => options[0] }); // reset to first option // no need for ngOnChanges anymore } ``` As we can see, our component code got only simpler with this, but this also implies some big things: when we start changing our more complex state properties, we may need to also alter our component code, and while those alterations will only be for the better, it is still time-consuming and may even be challenging from time to time. So, to recap this section, here are steps we need to take when converting complex properties to signals: 1. Change a property to be a signal of a value instead of the value itself ('true' -> 'signal(true)') 2. Be careful to call the signal in the template as a function: `open` \-> `open()` whenever necessary 3. Be careful not to confuse when we do **not** need to call the signal in the template, like with `[(ngModel)]` 4. Use `.update` instead of `.set` when dealing with complex properties or when you need the previous value 5. Consider using a library like Immer to help with very complex updates 6. Check our state properties for implicit dependencies and use `linkedSignal` or `computed` to handle them reactively 7. Do all of those steps in an incremental fashion, checking and testing along the way Now, that we got inputs and state properties out of our way, we can focus on the real heavy-weight: RxJS and signal interoperation. ### Migrating BehaviorSubjects Let's start this section with the most common and easy conversions: `BehaviorSubject`s. While they're probably not the most common Observables in Angular codebases, they are certainly the easiest to transform to signals. To understand this, let's quickly remind ourselves what a `BehaviorSubject` is: - it is an Observable that always has a value, - we can read that value at any time. - we can subscribe to it and perform some side effects when the value changes. This sounds a lot like a signal, doesn't it? In fact, we can convert a `BehaviorSubject` to a signal in a very straightforward way: ```typescript @Component({ selector: 'some-component', template: `

Current value: {{ value$ | async }}

`, }) export class SomeComponent { private value$ = new BehaviorSubject(0); constructor() { this.value$.subscribe(value => { console.log('Value changed:', value); }); } increment() { this.value$.next(this.value$.getValue() + 1); } } ``` Now, this `BehaviorSubject` can be converted to a signal like this: ```typescript @Component({ selector: 'some-component', template: `

Current value: {{ value() }}

`, }) export class SomeComponent { private value = signal(0); constructor() { effect(() => { console.log('Value changed:', this.value()); }); } increment() { this.value.update(v => v + 1); } } ``` Now, the steps are simple: 1. Change the `BehaviorSubject` to a signal of the same type: `new BehaviorSubject(0)` \-> `signal(0)`. 2. Change the template to call the signal as a function: `value$ | async` \-> `value()`. 3. Update the value updating logic to use the signal API: `this.value$.next(this.value() + 1)` \-> `this.value.update(v => v + 1)`. 4. Change the subscription to an effect: `this.value$.subscribe(...)` \-> `effect(() => {...})`. And this is it... for 95% of cases. However, if we are using some operators, especially asynchronous ones, we might need to do a bit more work. For instance, we might want to debounce emissions before we log new values if the user clicks the "Increment" button too fast: ```typescript @Component({ selector: 'some-component', template: `

Current value: {{ value$ | async }}

`, }) export class SomeComponent { private value$ = new BehaviorSubject(0); constructor() { this.value$.pipe( debounceTime(300) // wait for 300ms before emitting takeUntilDestroyed(), ).subscribe(value => { console.log('Value changed:', value); }); } increment() { this.value$.next(this.value$.getValue() + 1); } } ``` Now, let's move on to talk about converting RxJS Observables in general to signals and how to deal with operators we use. ### Migrating other Observables Let's continue with the previous example, and discuss two options we have before we move to chose one of them - Keep the `BehaviorSubject` as is, and use `toSignal` to convert it to a signal when we need it. - Convert the `BehaviorSubject` to a signal immediately and use `toObservable` whenever you need to use RxJS operators Both options are more or less solid, however the second one has a bit of an edge over the first one, since we would love to have as many signals everywhere we need reactive state, and it is also easier to access in the template or elsewhere (`someSignal()` vs `someBehaviorSubject$ | async` and `someBehaviorSubject$.getValue()`). So, let's reimagine our previous example while utilizing the `toObservable` operator to switch to RxJS in order to use operators like `debounceTime`: ```typescript @Component({ selector: 'some-component', template: `

Current value: {{ value() }}

`, }) export class SomeComponent { private value = signal(0); constructor() { toObservable(this.value).pipe( debounceTime(300), takeUntilDestroyed(), ).subscribe(value => { console.log('Value changed:', value); }); } increment() { this.value.update(v => v + 1); } } ``` Now, as we explored all the cases with `BehaviorSubject`, let's talk about Observables in general. Of course, the tool that is the easies to reach out to is the \`toSignal operator, however, let's be careful and realize we might not need it all the time. There are two glaring examples, the first is when we are using NgRx (or a similar state-management library that deals with Observables) and the second is when we are using an Observable of an HTTP request. In the case of NgRx, the lib itself provides a way to get signals instead of Observables, so we can use the `selectSignal` method to retrieve a signal of the state from a store selector: ```typescript @Component({ selector: 'some-component', template: `

Current value: {{ value() }}

`, }) export class SomeComponent { readonly #store = inject(Store); value = this.store.selectSignal(selectValue); // selectSignal returns a signal constructor(private store: Store) { effect(() => { console.log('Value changed:', this.value()); }); } increment() { this.store.dispatch(incrementValue()); } } ``` Great! No need to bother with `toSignal`, just use the signal straight away. Now, if we have an HTTP request, Angular now provides a separate reactive primitive, the `httpResource`, which handles the entire lifecycle of an HTTP request, like loading state, errors and so on: ```typescript @Component({ selector: 'some-component', template: ` @if (resource.isLoading()) {

Loading...

} @else if (resource.hasError()) {

Error: {{ resource.error() }}

} @else {

Data: {{ resource.data() | json }}

} `, }) export class SomeComponent { readonly resource = httpResource(this.#http.get('/api/data')); } ``` As we can see, this is super simple, and we are dealing exclusively with signals, no subscriptions, no `async` pipes, just signals. You can read more about resources in [one of my previous articles](https://www.angularspace.com/meet-http-resource/). > Note: `httpResource` currently is meant to work only for `GET` requests, and while you can force it to use a differernt method like `POST`, it is heavily discouraged, so, for now, we need to find other solutions for the rest of HTTP verbs, as using `toSignal` and so on So, when do we use `toSignal` then? Well, let's finally move to the last question we ai mto answer with this article to learn about that. ## When should I use signals and when RxJS? As we noticed throughout the article, signals were usually used as a synchronous but reactive storage for some values, while RxJS was used to handle asynchronous streams of events. This distinction is very important and useful: always think of signals as data (or state, as we call it), and RxJS as events. This will surely help us correctly identify when to use which. For instance, consider this: ```typescript fromEvent(document, 'click').pipe( map((event: MouseEvent) => event.clientX), takeUntilDestroyed(), ).subscribe((x) => { console.log('Mouse clicked at X:', x); }); ``` Here, we focus on user generated *events* and which are inherently asynchronous. Also, there's no concept of "state" or "data" here; we are simply reacting to events. So, this is a perfect use case for RxJS. Of course, we could use the `toSignal` function here, but what would that signal even be? It will always store the latest click event object, but is that even a useful semantic category? Not really, so we can safely disregard it and just use RxJS here. However, consider a scenario where we are using some API that returns a state in the form of an Observable (for instance, what NgRx would be if not for the `selectSignal` method). In this case, we are dealing with a state that is updated over time, and we can use the `toSignal` operator to convert it to a signal: ```typescript this.state$ = toSignal( this.store.select(selectState), {defaultValue: someDefaultValue} ); ``` So, we can always use this reasoning to determine whether we need to use signals or RxJS. Now, one burning scenario might be the one we already discussed: we have a reactive value, not a stream of events, but we really want to apply some (async) RxJS operators to it, like debouncing. In this case, as already mentioned, it would still be wise to have the value as a signal, since it's simpler to deal with, and then, in some place, convert it to an Observable, apply all the operators you like, and then subscribe to it. This way, we can still use the signal as a source of truth, but also add RxJS niceties to the mix. ## Conclusion Converting a huge Angular app to exclusively use signals is challenging, hard, and daunting task. But of course, it is more than achievable, and, as we saw in this article, can be done in a harmless, incremental way. While you will certainly encounter some complex situations along the way, hopefully this piece will help guide you along reaching your reactivity goals! ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) P.S If you want to learn more about RxJS interoperability in Angular, or dive deep into signals, check out the 5th, 6th and 7th chapters of my book ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/08/Screenshot-2024-11-13-at-15.52.00--1---3---1---1---1-_upscayl_2x_high-fidelity-4x--1-.jpg) ### First Angular Space Meetup Date!! URL: https://www.angularspace.com/first-angular-space-meetup-date/ Last updated: 2025-08-20T14:58:46.000Z Finally!!! First Angular Space Meetup Date!! Signals are rewriting how we think about reactivity in Angular → From v16 signal/computed/effect → to stabilized resources in v20 → to upcoming forms & router improvements… the journey has just begun. That’s why our first Angular Space Meetup is all about “The Future of Signals.” **📅 August 27, 5 PM UTC** 🎙 **Host: Armen Vardanyan** – Angular GDE, author of Modern Angular, One of the Top Angular Space Authors 🎤 **Guest: Michael Small** – Frontend Lead at Relationship One, contributor to ngrx-toolkit & creator of allEventsSignal in ngxtension We’ll cover: ✅ Signals in greenfield vs existing projects ✅ Libraries embracing signals ✅ RxJS vs Signals tradeoffs & interop experiences ✅ Migration strategies & caveats ✅ Where signals fit best in production 🔴 Live on X, YouTube, Twitch & LinkedIn! Organized by Angular Space 🌌 **For Live Stream on X visit my profile** [**https://x.com/DanielGlejzner**](https://x.com/DanielGlejzner?ref=angularspace.com) **For Live Stream on LinkedIn visit my profile** [**https://linkedin.com/in/daniel-glejzner-271281159/**](https://linkedin.com/in/daniel-glejzner-271281159/?ref=angularspace.com) **For Live Stream on YouTube** [**https://youtube.com/@AngularSpaceMeetup**](https://youtube.com/@AngularSpaceMeetup?ref=angularspace.com) **For Live Stream on Twitch** [**https://twitch.tv/angularspacemeetup**](https://twitch.tv/angularspacemeetup?ref=angularspace.com) ### 5 TypeScript Utility Types You Can't Live Without URL: https://www.angularspace.com/5-typescript-utility-types-you-cant-live-without/ Last updated: 2025-08-18T06:51:44.000Z # I decided to share a collection of custom utility types that I use in my daily work. Maybe you'll find them useful, maybe not, but it's worth knowing that creating custom types really gets the job done, especially when building strongly-typed and safe code. Perhaps it will inspire you to create your own types that solve the problems you encounter every day? Let's dive in! ## 1\. Handling State Transitions with `Process` Ever found yourself doing something like this? ```typescript type State = { loading: boolean; error: string | null; data: User | null; }; const state: State = { loading: true, error: "", data: null, }; ``` If you do this, you're just hurting yourself, because later your code looks like this (and that's just the beginning) - I talk more about this in my article [Exhaustiveness Checking And Discriminant Property: The Complete Guide](https://4markdown.com/exhaustiveness-checking-and-discriminant-property-the-complete-guide/?ref=angularspace.com). ```typescript if (loading && !error && data) {} ``` The **discriminant property** (using Discriminated Unions) comes to the rescue. ```typescript type State = | { status: "idle" } | { status: "busy" } | { status: "ok"; data: User } | { status: "fail"; error: string }; const state: State = { status: "idle" }; ``` This allows us to directly and quickly determine which "variant" of the state we're dealing with. If it's the **ok** variant, we can access the **data** field, and if it's "fail", we can access the **error** field. For the other two, there's no such option. This greatly simplifies the code and eliminates extra "nulls". ```typescript if (state.status === "ok") { // accessing data is safe here! } ``` However, repeating this structure everywhere generates a lot of duplication, and there are many states like "fetch, show, and handle error" or "save and show message". A custom `Process` type can help with this. ```typescript // utility-types.ts /** * Represents the state of an asynchronous process. * Defaults TData to void (no data) and TError to Error. */ type Process = | (TSkipIdle extends false ? { status: "idle" } : never) | { status: "busy" } // If TData is void, 'data' property is omitted | (TData extends void ? { status: "ok" } : { status: "ok"; data: TData }) // If TError is void, 'error' property is omitted | (TError extends void ? { status: "fail" } : { status: "fail"; error: TError }); // Usage Examples: const state: Process = { status: "idle" }; // Success case const successState: Process = { status: 'ok', data: { id: '1', name: 'Test' } }; // Failure case with default Error type const errorState: Process = { status: 'fail', error: new Error('Failed to fetch') }; // "idle" removed because we don't need it in this specific case const stateWithoutIdle: Process = { status: 'busy' }; ``` Notice that we also have the option to remove the 'idle' status using `TSkipIdle`. Sometimes the flow is just `busy -> ok -> fail` because we don't want an "idle" status for a "get" operation. This would generate extra code and increase **cyclomatic complexity**, as our application simply doesn't need that status in such cases. Maybe you're loading data immediately when a component is mounted, so there's no need to call an additional state update just to switch from `idle` to `busy`. It can be used in this way as a complete example to demonstrate its power. ```typescript type State = Process let state: State = { status: "idle" }; const getUser = async (userId: string) => { state = { status: "busy" }; try { const user = await apiCallToUser(userId); state = { status: "ok", data: user }; } catch (error) { state = { status: "fail", error: "Something went wrong" }; } }; ``` Instead of something like this. ```typescript type State = { loading: boolean; error: string | null; data: User | null; }; let state: State = { loading: true, error: "", data: null, }; const getUser = async (userId: string) => { state = { loading: true, error: null, data: null }; try { const user = await apiCallToUser(userId); state = { loading: false, error: null, data: user }; } catch (error) { state = { loading: false, error: "Ups", data: null }; } }; ``` Both the read and update logic are cleaner and less complex in terms of **cyclomatic complexity**. There are four possible states instead of 2 (loading) × 2 (error) × 2 (data) = 8 combinations. There's also no risk of mistakenly updating the state (e.g., forgetting to set the loading flag to false and ending up with an infinite loader). A bit cleaner, right? > As I mentioned before, this is just one use case for this utility type. A full explanation of the problems you may encounter when defining your state variants with flags is available in the [Exhaustiveness Checking and Discriminant Property: The Complete Guide](https://4markdown.com/exhaustiveness-checking-and-discriminant-property-the-complete-guide/?ref=angularspace.com) article. ## 2\. Handling API Communication with `Result` When working with `fetch` or `axios`, if a promise is rejected and you forget to add a `.catch()` block, you'll end up with an unhandled rejection. Sometimes, an API might even return a different data shape for errors instead of rejecting the promise, which complicates handling. Of course, this is perfectly valid behavior, but what if we could make it simpler? What if we had a type that models this common scenario more elegantly? For instance: `{ status: "aborted" } | { status: "fail" } | { status: "ok" }`. It could look like this: ```typescript type Result = | { status: 'aborted' } | { status: 'fail'; error: unknown } | { status: 'ok'; data: TData; }; // It returns Result const result = await service('https://api'); if (result.status === 'aborted') return; if (result.status === 'fail') { alert('Oops! ' + result.error); return; } if (result.status === 'ok') { alert('WORKS!'); return; } // This final part serves as a safeguard. It uses TypeScript's exhaustiveness // checking to ensure that every possible status is handled. If you were to // comment out one of the `if` blocks above, TypeScript would throw an error on // the following line, because the `result` variable could still hold a value // (e.g., { status: 'aborted' }) that cannot be assigned to the `never` type. const _exhaustiveCheck: never = result; // This trick ensures at compile time that all cases are handled. ``` I've omitted the implementation of `service` because it may look different in the project context. Sometimes it may be more "generic", and sometimes it may be more connected to the project domain. In addition, this article is about types, not runtime. So, implementing such a service may be a good exercise for you. > Especially in React, this approach to modeling API responses is very helpful. You can easily abort stuff in `useEffect`, and automatically return `void 0` to avoid calling set-state operations or anything else when the hook is unmounted or a dependency in the array has changed. ## 3\. Avoid Primitive Obsession with `Brand` **Primitive obsession** is a code smell where developers use simple primitive types, like `string` or `number`, to represent more complex, domain-specific concepts. This approach is often too generic and can lead to subtle bugs, like the one shown below: ```typescript const getUserPosts = (userId: string) => { // Logic that queries for users } const documentId = 'doc-xyz-123'; // also a string // TypeScript sees no issue here, but it's a nasty bug! getUserPosts(documentId); ``` We can prevent this by creating a **branded type**, which blocks such invalid assignments unless an explicit type cast is performed. ```typescript type Brand = TData & { __brand: TLabel }; type UserId = Brand; ``` This works by creating an intersection type that combines the primitive (e.g., `string`) with a unique, phantom property like `__brand`. A regular string lacks this property, so TypeScript considers it a different type, all without adding any runtime overhead as the brand is erased during compilation. ```typescript const getUserPosts = (userId: UserId) => { // ... } const documentId = 'doc-xyz-123'; // This is just a plain string // Now TypeScript throws an error 💢, preventing the nasty bug. // Error: Argument of type 'string' is not assignable to parameter of type 'UserId'. getUserPosts(documentId); // To create a UserId, you must explicitly cast it (ideally within a validation function): const userId = "user-456" as UserId; getUserPosts(userId); // OK ``` ## 4\. Make Your Types Beautiful with `Prettify` Have you ever created a complex type using TypeScript's utility types like `Pick`, `Omit`, or intersections (`&`), only to hover over it and see a long, confusing definition in your editor? Instead of a clean, flat object, TypeScript often shows you the entire formula used to create the type. This technique, popularized by [Matt Pocock](https://www.mattpocock.com/?ref=angularspace.com), solves that. For instance, a complex, computed type might look like this in your editor's IntelliSense tooltip: ![Ugly type definition image](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/OWnL9ANsCfO1FxOyeDx918LFnFH3%2Fimages%2F574a3d7b-0f94-4971-a9fa-97ac927e0f35?alt=media) With the `Prettify` utility, we can make it look clean and readable. ```typescript type Prettify = { [Key in keyof TObject]: TObject[Key]; } & {}; ``` This simple utility type works by iterating over all the properties of the input object (`TObject`) and explicitly mapping them into a new object structure. The `& {}` at the end is a clever trick that forces TypeScript to evaluate this new structure and display the flattened, final object type instead of the underlying complex one. Now, when you apply `Prettify`, you get a much nicer-looking type definition: ![Formatted type definition](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/OWnL9ANsCfO1FxOyeDx918LFnFH3%2Fimages%2F322e06d9-647c-47b1-9b20-e37b5ffa66f8?alt=media) > Don't spam it everywhere now. Just use it in cases where working with types is really hard :D. ## 5\. Type Your Routes Safely with `StrictURL` Manually building URLs with string concatenation (`'/users/' + userId`) is fragile and prone to runtime errors. We can enforce a correct URL structure at compile time using a `StrictURL` utility type, which leverages some of TypeScript's more advanced features to create fully type-safe URLs. ```typescript // Recursively joins path segments with a "/". Does not add a trailing slash. type ToPath = TItems extends [ infer Head extends string, ...infer Tail extends string[], ] ? `${Head}${Tail extends [] ? '' : `/${ToPath}`}` : ''; // Recursively builds a query string from parameter names type ToQueryString = TParams extends [ infer Head extends string, ...infer Tail extends string[], ] ? `${Head}=${string}${Tail extends [] ? '' : '&'}${ToQueryString}` : ''; // The main utility to construct the full URL type type StrictURL< TProtocol extends 'https' | 'http', TDomain extends `${string}.${'com' | 'dev' | 'io'}`, TPath extends string[] = [], TParams extends string[] = [], > = `${TProtocol}://${TDomain}${TPath extends [] ? '' : `/${ToPath}`}${TParams extends [] ? '' : `?${ToQueryString}`}`; ``` ### Breaking Down the Magic This utility looks complex, but its power comes from two key concepts working together: 1. **The `infer` Keyword:** This is the core of the trick. `infer` allows us to declare a variable for a type *inside* a conditional check. In `[infer Head, ...infer Tail]`, TypeScript captures the type of the first array element into a new type variable `Head` and the type of the rest of the array into `Tail`. It's essentially destructuring for types. 2. **Recursive Conditional Types:** The type calls itself with the `Tail` of the array. This creates a "loop" that processes each segment of the path or query string one by one. The loop stops when the array is empty (the "base case"), at which point it returns an empty string, and the results are combined into the final string literal type. In short, `ToPath` recursively pulls the `Head` off the array, appends a slash if there is more to process, and processes the `Tail`, until nothing is left. `ToQueryString` does the same but formats the output for URL parameters. ### Usage Examples in Action This allows TypeScript to compute the final URL structure, giving you incredible autocompletion and error-checking. ```typescript // A route with a dynamic segment type HomeRoute = StrictURL<'https', 'polubinski.io', ['articles', string, 'id']>; // Hovering shows: `https://polubinski.io/articles/${string}/id` // A route with query parameters type SearchRoute = StrictURL<'https', 'google.com', ['search'], ['q', 'source']>; // Hovering shows: `https://google.com/search?q=${string}&source=${string}` ``` This pattern is used by modern frameworks like Next.js to provide type-safe routing out of the box, preventing an entire class of bugs. ## Summary I have many more such beauties, but I'll save them for future articles. The last one was quite complex, but to be honest, it nicely fits into the strict and type-safe direction the industry is heading. AI tools can help craft these advanced utilities. In turn, these strong types create a stricter codebase, making it easier for both human developers and AI assistants to find and fix problems. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/08/Screenshot-2024-11-13-at-15--3-.jpg) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/08/angular-university-banner-4--3-.jpg)](https://angular-university.io/?ref=angularspace.com) ### Senior Angular Interview Questions URL: https://www.angularspace.com/senior-angular-interview-questions/ Last updated: 2025-07-29T15:23:50.000Z Lately, I've been interviewing candidates for a Senior Angular Developer role, and I've ended up rejecting the majority of them. Am I a jerk for that? Maybe. But here's what I've realized from these interviews: we live in a bubble. Most of us spend our day-to-day work calling APIs, caching data, and displaying it to users. We do this for years, and it leads us to believe we've mastered frontend development, especially a framework like Angular. With this article, I want to walk through some of the technical questions I like to ask during interviews, all while exploring a deeper thought: **"Am I really a senior developer, or do I just think I am?"** Before diving into the questions and sample answers, there's a broader discussion worth mentioning: **Who's more valuable - a generalist or a specialist?** Someone who's worked across many domains (CI/CD, backend, databases, frontend, etc.), or someone who's deeply specialized in a single technology and only has surface level knowledge of the rest? That's a question only the interviewer can answer, based on the needs of the team. In my case, I was always looking for a specialist, hence following list of Angular focused interview questions. Finally, this is my personal list, feel free to critique or customize it for your own hiring process. - General Questions: - What does it mean to be a senior developer ? - Do you prefer declarative or imperative programming? - Would you use a state management library or a custom implementation? - Angular Questions - General: - How would you achieve a parent - child component communication ? - What is the role of `NgZone` in Angular, and when would you opt out of Angular's change detection? - What is and when to use an Injection Token ? - What are resolution modifiers and how to use them ? - Why would you use a `track` function in a for-loop and how it works ? - What is the the difference between `providers` and `viewProviders` ? Can you provide an example when to use either of them ? - Why `pipes` are considered safe in a template, but regular function calls (not signals) are not ? - Angular Questions - Signals: - How would you convince your team to migrate a project from Observables to signals ? - Can you explain the diamond problem in Observables and why it doesn't occur with signals? - When to use `effect` and `untracked` in a signal based application ? - Are life-cycle hooks still needed in a fully signal based application ? - Angular Questions - RxJS: - What is a higher-order observable and how they differ ? - What is the difference between `share()` and `shareReplay()` ? - What does this code do ? - `scan()` \+ `expand()` ## General Questions These are my go to questions at the very start of the interview process. There are no right or wrong answers here, what I'm really interested in is the developer's personality and thinking style. After all, this is someone who will be working closely with me and the rest of the team, and ideally, we want people who share a similar mindset or programming philosophy. ### What does it mean to be a senior developer ? This question helps me understand how I should approach the person I'm interviewing. As I mentioned earlier, there's often a pendulum swing between being a generalist and a specialist, so I like to hear how the candidate defines a senior developer. Most answers I get sound something like: > "*Understanding the framework (Angular) very well and mentoring junior developers.*" That's a pretty standard response, and I usually follow it up with: > "*Alright, so if a senior should know the framework really well, is it okay for me to ask hard questions and expect you will be able to provide answers for them?*" What I'm really trying to grasp is how the candidate sees themselves, whether they feel they've reached a senior level and whether they've dealt with complex, challenging problems. This way, we set some expectations for the interview process, not defined by me, but by the candidate themselves. Personally, I have a slightly broader view of what makes someone a senior developer. Yes, you should understand the framework well, but I'd also expect you to: - Initiate and lead technical improvements, like addressing tech debt, and be able to pitch those ideas to higher ups. - Push back (respectfully) on product or feature decisions that don't make sense or may harm long-term goals of the product. - Know how to give and receive feedback during code reviews. - Recognize when to Google something, when to ask a peer, and when it's worth gathering a few people for a brainstorming session. - Care about the product and collaborate closely with product owners. ### Do you prefer declarative or imperative programming? You might say this is a theoretical question, and it is, but I like to ask it because it reveals how candidates think about code structure and maintainability. Most candidates respond with, *“I don't know the difference”*. If you Google this question, you'll see something like: “*Declarative programming focuses on what needs to be done, while imperative programming focuses on how to do it”*. In Angular terms, here's one way to think about it. Imperative programming involves mutating variables in multiple places, often with manual logic to track side effects. Declarative programming, by contrast, involves defining a value or behavior in one place, often through `computed`, `signal`, or `RxJS` streams. I highly recommend checking out [Joshua Morony's video](https://www.youtube.com/watch?v=Fr9e9Fo6iMw&ref=angularspace.com) on this topic. Here is a coding example: ```typescript // Imperative Programming @Component({ template: ` ... ` }) export class ChildComponent { private readonly wsService = inject(WsService); private readonly apiService = inject(ApiService); displayedData = signal([]); constructor(){ // load existing data this.apiService.existingData$.subscribe((data: string[]) => { this.displayedData.set(data); }); // listen on WS new data push this.wsService.newData$.subscribe((data: string) => { this.displayedData.update((current) => [...current, data]); }); } } ``` ```typescript // Declarative Programming @Component({ template: ` ... ` }) export class ChildComponent { private readonly wsService = inject(WsService); private readonly apiService = inject(ApiService); displayedData = toSignal( merge(this.apiService.existingData$, this.wsService.newData$).pipe( scan((acc: string[], curr: string) => [...acc, curr], [] as string[]) ), { initialValue: [] }); } ``` The imperative version manually subscribes to two streams (`existingData$` and `newData$`) and mutates the `displayedData` signal in separate steps. Each data source is handled independently, which can lead to duplicated logic and harder maintenance as complexity grows. In contrast, the declarative version merges both streams and uses `scan` to build up the `displayedData` in a single, unified expression. It avoids manual subscriptions and keeps logic in one place. This makes the code more predictable, easier to test, and less errors. Overall, the declarative approach describes what should happen, while the imperative one controls how it happens. ### Would you use a state management library or a custom implementation? The question is designed to brainstorm with the candidate. Some developers lean toward libraries like NgRx, Akita, or NGXS, while others prefer simpler, custom built state solutions using services, RxJS or signals. Both approaches are valid. What I'm really curious about is whether the candidate can justify their choice, present some trade-offs, and mention potential drawbacks of the alternative. A senior developer should articulate decisions clearly, even when their opinion differs from their peers or contradicts the tech stack we are currently using. The provided answer will not change how I look at the candidate, I just want to see how can they argue toward one solution they see fit. ## Angular Questions - General ### How would you achieve a parent - child component communication ? A simple question with a catch. Most candidates mention `@Input()`/`@Output()` bindings, or using a shared service with a `Subject` or a signal for communication. ```typescript // Input/Output example @Component({ selector: 'app-child', template: ` ... ` }) export class ChildComponent { cSelected = output(); cData = input(); } @Component({ imports: [ChildComponent], template: `` }) export class ParentComponent { pData = signal(['a', 'b', 'c']); pSelected = signal(''); } ``` ```typescript // Shared Service example @Injectable({ providedIn: 'root' }) export class SharedService { store = signal(undefined); } @Component({ selector: 'app-child', template: ` ... ` }) export class ChildComponent { service = inject(SharedService); onPushData(){ this.service.store.set(['a', 'b', 'c']); } } @Component({ imports: [ChildComponent], template: `` }) export class ParentComponent { storedData = inject(SharedService).store } ``` These are valid answers, but not enough for a senior level developer. I expect to also hear about: - Custom two-way binding - Model inputs - Control Value Accessor **Custom two-way binding** is achieved when the child component has an input and an output with the same property name, but the output uses the `Change` suffix. This syntax enables the “banana-in-a-box” `[(data)]` binding in the template. When the child emits a value via `cDataChange.emit('something')`, it directly updates the parent's `pData` signal or property. ```typescript @Component({ selector: 'app-child', template: ` ... ` }) export class ChildComponent { cData = input(''); cDataChange = output(); onDataChange(){ this.cDataChange.emit('something'); } } @Component({ imports: [ChildComponent], template: `` }) export class ParentComponent { pData = signal('Hello World'); } ``` **Model inputs** offer syntactic sugar over manual two-way binding. Instead of defining two separate properties (`@Input` and `@Output`) and emitting values manually, you can use `model()` to bind once and let Angular handle the rest. The `model()` binding works both with signals and non signal properties passed from the parent. A common use case is within custom form controls. This is not yet a signal form example, but so far the closest we can get. Inside a child component, we display an input (custom input, maybe with some specific functionality), and whenever the user types something, the `ngModel` emits the data into `cData` , and since it's a `model()`, it will once again emit the data to the parent `pData`. ```typescript @Component({ selector: 'app-child', imports: [FormsModule], template: ` ` }) export class ChildComponent { cData = model(''); } @Component({ imports: [ChildComponent], template: ``, }) export class ParentComponent { pData = signal('Hello World'); } ``` **Control Value Accessor (CVA)** is ideal when the child component acts as a custom form control. Implementing `ControlValueAccessor` allows the component to integrate with Angular's forms APIs, either reactive or template-driven. I mostly use [control value accessor](https://angular.dev/api/forms/ControlValueAccessor?ref=angularspace.com) when creating reusable component in a UI library or when the child is something like a custom search-select component. Imagine searching for goods in an amazon like webapp, that when you type a good's prefix, it makes an API call, and then you have a dropdown of possible options. ```typescript @Component({ selector: 'app-custom-input', imports: [FormsModule], template: ``, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomInputComponent), multi: true }] }) export class CustomInputComponent implements ControlValueAccessor { value = ''; // callbacks for the ControlValueAccessor private onChange = (value: string) => {}; private onTouched = () => {}; // called when input changes onInput(value: string): void { this.value = value; this.onChange(this.value); // propagate change this.onTouched(); // mark as touched } // required from ControlValueAccessor writeValue(value: string): void { this.value = value; } // required from ControlValueAccessor registerOnChange(fn: (value: string) => void): void { this.onChange = fn; } // required from ControlValueAccessor registerOnTouched(fn: () => void): void { this.onTouched = fn; } } ``` ```typescript @Component({ selector: 'app-parent', imports: [ReactiveFormsModule, CustomInputComponent], template: `

Parent value: {{ pDataControl.value }}

` }) export class ParentComponent { pDataControl = new FormControl('Hello World'); } ``` **Control Value Accessor** looks more complicated and takes time to implement, especially when creating a complex custom part of the form, since it allows integrations with reactive or template driven forms. Worth mentioning, that some candidates also bring up using `viewChild()` to reference the child component from parent, or using local storage or cookies to pass data, which are valid answers, but I would avoid these solutions in production. ### What is the role of `NgZone` in Angular, and when would you opt out of Angular's change detection? The answer on this question can really demonstrate the candidate skills and the level of projects he has work on. You rarely run code outside of Angular's change detection, you consider it when you run into performance issues. NgZone is a wrapper around JavaScript's event loop that allows Angular to know when to trigger change detection. Angular patches async operations like `setTimeout`, `Promise`, `XHR`, etc. using `zone.js`, so when those operations complete, Angular automatically runs change detection to update the view. This occasionally leads into performance issues if you're running lots of non UI related or high frequency code (scroll, setInterval). In those cases, you can opt out of Angular's change detection using `NgZone.runOutsideAngular()`, and manually re-enter with `NgZone.run()` only if needed. For a practical example, I include my blogpost - [Simple User Event Tracker In Angular](https://www.angularspace.com/simple-user-event-tracker-in-angular/), where I setup some global listeners on button clicks, input or select changes. They do not impact UI bindings, and the code runs in the background, therefore we can run it outside Angular's change detection system. Other options where to reach it for this options include integrating with analytics, tracking, or 3rd-party scripts that are passive. ```typescript @Injectable({ providedIn: 'root' }) export class ListenerService { private trackerService = inject(TrackerService); private document = inject(DOCUMENT); private ngZone = inject(NgZone); constructor() { this.ngZone.runOutsideAngular(() => { this.document.addEventListener('change', (event) => { const target = event.target as HTMLElement; if (target.tagName === 'INPUT') { this.trackerService.createLog({ type: 'INPUT', value: (target as HTMLInputElement).value, }); } // others .... }, true); }); } } ``` ### What is and when to use an Injection Token ? An [InjectionToken](https://angular.dev/api/core/InjectionToken?ref=angularspace.com) is like a unique identifier or a name tag that Angular uses to locate and provide a specific value or service during dependency injection. You typically use `new InjectionToken()` when you want to provide a value that isn't a class such as a configuration object, primitive value, or interface-based dependency. One common use case is running initialization logic at the app startup using the [APP\_INITIALIZER injection token](https://github.com/angular/angular/blob/main/packages/core/src/application/application%5Finit.ts?ref=angularspace.com) token. While `APP_INITIALIZER` is now deprecated, the recommended replacement is the [provideAppInitializer](https://angular.dev/api/core/provideAppInitializer?ref=angularspace.com) function. ```typescript bootstrapApplication(App, { providers: [ provideAppInitializer(() => { // init languages // get data from cookies // setup sentry // etc ... }), ], }); ``` You can also define custom injection tokens, commonly used when developing Angular libraries that require configuration from the consuming app. For instance, if your library makes API call and needs to know whether it should call a production or development API, the consumer can provide this value through a token. ```typescript // code in the library export const API_ENDPOINT = new InjectionToken('API_ENDPOINT'); // -------------- // in a different application/library bootstrapApplication(AppComponent, { providers: [ { provide: API_ENDPOINT , useValue: '/prod/api' }, ] } ``` ### What are resolution modifiers and how to use them ? A great explanation on this topic can be found in [Decoded Frontend - Resolution Modifiers (2021)](https://www.youtube.com/watch?v=uVGnsmm9g-I&ref=angularspace.com). While the video is slightly dated, but the concepts remain the same. When injecting a service, Angular allows you to configure up to four resolution modifiers via the second argument to the `inject()` function. Below is a brief overview of each, focusing on what I typically expect from a senior candidate. ```typescript private service = inject(SomeService, { host: true, optional: true, self: true, skipSelf: true }); ``` **Optional()** is used when the provided service / injection token may or may not be provided by the developer. Example is the [APP\_INITIALIZER Injection token](https://github.com/angular/angular/blob/aa52cd41fd94c952cbaa53ae6e1db99f9254871c/packages/core/src/application/application%5Finit.ts?ref=angularspace.com#L223). When Angular injects this token, it uses `inject(APP_INITIALIZER, {optional: true})` , since you, as a developer can, but don't have to provide some executable code when angular initiates. **Self()** forces Angular to resolve the dependency only from the current injector. It won't check parent injectors. This is especially useful in directives that should only operate on the element they're attached to. An example is adding an asterisk to required input fields. You use `self` when injecting `NgControl`, so it only pulls from the target element: ```typescript @Directive({ selector: 'input[formControlName], input[formControl]' }) export class RequiredMarkerDirective { private ngControl = inject(NgControl, { optional: true, self: true }) constructor() { if (this.ngControl?.control?.hasValidator(Validators.required)) { // Add red asterisk } } } ``` Angular itself uses `self()` internally, for example in `ReactiveFormsModule` or `FormsModule` to resolve [sync and async validators used on the form](https://github.com/angular/angular/blob/aa52cd41fd94c952cbaa53ae6e1db99f9254871c/packages/forms/src/directives/ng%5Fform.ts?ref=angularspace.com#L171). **SkipSelf()** is the opposite of `self`. It tells Angular to skip the current injector and look in the parent. This is useful when a directive or component needs to interact with a container element, like a parent form. In the example below, when using the `FormControlName` directive on reactive forms, it tries to resolve the [parent form name for the control](https://github.com/angular/angular/blob/main/packages/forms/src/directives/reactive%5Fdirectives/form%5Fcontrol%5Fname.ts?ref=angularspace.com#L150). ```typescript @Directive({ selector: '[formControlName]', providers: [controlNameBinding], standalone: false, }) export class FormControlName extends NgControl implements OnChanges, OnDestroy { constructor( @Optional() @Host() @SkipSelf() parent: ControlContainer, // ... other injectors ) } ``` **Host()** modifier limits resolution to the host component or directive. It prevents Angular from searching up the hierarchy. For instance, if a directive inside `FinalComponent` tries to inject `FormGroupDirective` using `@Host()`, Angular will only look inside `FinalComponent`, not any parent components that may contain the actual form. ```typescript @Directive({ selector: '[appHostFormDirective]', }) export class HostFormDirective { private formGroup = inject(FormGroupDirective, { host: true }) constructor() { console.log('FormGroupDirective found:', formGroup); } } ``` ```typescript @Component({ selector: 'app-final', template: `
`, imports: [ReactiveFormsModule, HostFormDirective], }) export class FinalComponent { form = new FormGroup({ name: new FormControl(null), }); } ``` I rarely use these modifiers in day-to-day application development. They tend to become more relevant when building libraries or advanced directives. However, Angular itself uses them extensively, and reviewing its source code is a great way to see them applied effectively. ### Why would you use a `track` function in a for-loop and how it works ? The [track function](https://angular.dev/api/core/TrackByFunction?ref=angularspace.com) is a useful performance optimization that was often overlooked with the old `*ngFor="let item of items"` syntax. Fortunately, the new [control flow @for()](https://angular.dev/tutorials/learn-angular/5-control-flow-for?ref=angularspace.com) now requires you to specify a `track` function, which encourages better practices. So, why is it important? Imagine you have a component that makes an API call to fetch a list of items, list of users, and displays them in the template. You also have a “reload” button to refetch this data (in case something has changed on the backend). Below is an example using the older `*ngFor` syntax to illustrate the issue: ```typescript @Component({ selector: 'app-child', imports: [NgForOf], template: `
{{item.name}}
` }) export class ChildComponent { items = signal<{ id: string; name: string }[]>([]); onRerun() { // "fake api call" to reload data this.items.set([{id: '100', name: 'Item 1'}, /* ... */ ]); } } ``` In this setup, every time `onRerun()` is triggered and the array is updated (even with the same content), Angular will re-render all elements in the DOM. That's because it can't tell which items stayed the same and why didn't. It result to performance loss and UI flickering, especially in long or complex lists. To prevent this, you use a `trackBy` function: ```typescript @Component({ selector: 'app-child', imports: [CommonModule], template: ` ` }) export class ChildComponent { // ... previous code identify(index: number, item: { id: string }): string | number { return item.id; } } ``` This tells Angular how to uniquely identify items in the array, commonly via an `id`. With a `trackBy` function (or `track` key in `@for()`), Angular can associate each item with its corresponding DOM element. When the data is reloaded, Angular compares these keys (not full object references), allowing unchanged items to be preserved in the DOM. Why does this matter? Because DOM operations are expensive. Without proper tracking, Angular discards and recreates DOM elements for every item, even if the data hasn't changed. With tracking, the DOM elements stay in place, and Angular only updates bindings when necessary. On the GIF below, the top list uses `trackBy: identify` while the second one does not. You can see the difference. The top list preserves DOM elements during data reload, whereas the second recreates them entirely each time. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/ng-for-retrigger-reload.gif) NgFor retrigger without trackBy With the new `@for()` syntax, Angular enforces the use of a `track` key for the same purpose. However, two common mistakes still happen: - Using the object itself as the key - example: `@for (item of items(); track item)`. This does not work as expected because the reference to each item changes on every reload, even if the data is identical and it will re-render the UI every time, basically ignoring the `track` function. - Using `$index` as the key - example: `@for (item of items(); track $index)`. This causes problems when an item is removed. Suppose you delete the 5th item in a list of 10, then every item after index 4 now has a new index, forcing Angular to re-render them all unnecessarily. In stateful components like forms, this leads to loss of input focus or cursor position, however using the `$index` is okay for static lists. Here's a comparison: the top row uses `track item.id`, and the second uses `track $index`. Watch how the first preserves DOM elements during removal. Here is a [stackblitz example to play with](https://stackblitz.com/edit/stackblitz-starters-dtbhl8zb?file=src%2Fmain.ts&ref=angularspace.com). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/ng-for-index.gif) For loop using index for trackBy ### What is the the difference between `providers` and `viewProviders` ? Can you provide an example when to use either of them ? A great write-up on this topic is by [Paweł Kubiak](https://www.angularspace.com/author/pawel/) in his article [Hidden Parts of Angular: View Providers](https://www.angularspace.com/hidden-parts-of-angular-view-providers/). Below is a summary of his explanation, followed by a practical example. > *“When you use providers, the service is available to the component itself, its template, any child components, and even to content projected into it using ``.* > > *On the other hand, `viewProviders` limit the service's visibility strictly to the component's view. That means it's accessible only to the component and the elements declared directly in its template—but not to projected content or external child components.”* In most applications I've worked on, using `providers` or `viewProviders` were rare use-cases. Where I've seen this being showcased the most are [examples with NGRX](https://ngrx.io/guide/signals/signal-store?ref=angularspace.com#providing-and-injecting-the-store) and generating dynamic components with configurable dependencies. Let's take an example from a flight booking portal. On the final payment step you present two payment options - Stripe (default) and PayPal payments, allowing users to choose. Each option has a different implementation, but both rely on a common `PaymentService` abstraction: ```typescript export abstract class PaymentService { abstract pay(): void; } @Injectable() export class StripeService implements PaymentService { pay() { console.log('Paid with Stripe!'); } } @Injectable() export class PaypalService implements PaymentService { pay() { console.log('Paid with PayPal!'); } } @Component({ selector: 'app-payment-button', template: ``, }) export class PaymentButtonComponent { private paymentService = inject(PaymentService); handlePayment() { this.paymentService.pay(); } } ``` Of course in real life you would need to establish a connection with the payment provider, handle errors, etc. The `PaymentButtonComponent` button is using the abstract `PaymentService`, which means, we need to provide an instance of either the Paypal or Stripe service. To dynamically decide which implementation to use based on user selection, you can manually create and inject the appropriate provider. This example demonstrates destroying and re-instantiating the component with a different `PaymentService` provider each time: ```typescript @Component({ imports: [FormsModule], template: ` ` }) export class TestComponent { readonly usePaypal = signal(false); readonly container = viewChild('container', { read: ViewContainerRef }); constructor() { // init payment button effect(() => { const container = this.container(); const usePaypal = this.usePaypal(); untracked(() => { if (container) { this.loadComponent(container, usePaypal); } }); }); } loadComponent(vcr: ViewContainerRef, usePaypal: boolean) { // remove previous vcr.clear(); const injector = Injector.create({ providers: [ { provide: PaymentService, useClass: usePaypal ? PaypalService : StripeService } ] }); // attach component to DOM vcr.createComponent(PaymentButtonComponent, { injector }); } } ``` This example shows how `providers` can be dynamically configured depending on runtime logic. We don't use `providedIn: 'root'` here, even if our services were globally provided, because using `Injector.create()` always [results in new instances](https://angular.dev/api/core/Injector?ref=angularspace.com#create), overriding any singleton behavior. Even if the candidate doesn't know the exact difference, that's still okay, but I would expect at least one example where they encountered a situation that a global service wasn't enough, they needed to use a provider to create multiple instances for whatever reason. ### Why `pipes` are considered safe in a template, but regular function calls (not signals) are not ? Pure pipes are only reevaluated when their input values change, which makes them efficient and safe to use in templates. On the other hand, function calls in templates are executed on every change detection cycle. So while `{{ name | uppercase }}` in the template is safe, `{{ someHeavyFunction() }}` will be called many times per second, which is rarely what you want. As [Angular docs say](https://angular.dev/guide/templates/pipes?ref=angularspace.com#change-detection-with-pipes): “*by default all pipes are considered `pure`, which means that it only executes when a primitive input value is changed.*” Here I do a shameless plug including my article where I dove deeper into how the [Implementation of Angular Pipes works](https://dev.to/this-is-angular/deep-dive-into-angular-pipes-implementation-2g5n?ref=angularspace.com). Under the hood, pipes create a caching object, and for a specific input, they perform the pipe's logic and store the result in the cache. Then, when change detection runs again with the same input, the pipe first checks the cache. If the output was already computed, it simply returns the cached value, making it an O(1) operation. ## Angular Questions - Signals Signals in Angular were first introduced in May 2023 with version 16, and there was quite a bit of buzz even before their official release. What fascinates me is when a candidate says, *“Yeah, signals are here, but we worked on an older project and never migrated, so I never had a chance to try them out.”* That's a red flag… what else can I say? As a senior developer, you're expected to have an understanding of newer features and how they work, even if you haven't used them in production yet. ### How would you convince your team to migrate a project from Observables to signals ? This question tells me two things. First, whether the person has a solid understanding of signals, and second, whether they've ever initiated a larger tech debt refactor on a project. I believe a senior developer should actively drive technical improvements and come forward with such initiatives. One solid answer might be something like: > *“Angular, and overall the whole frontend ecosystem, is clearly moving toward signals. There's even a [TC39 proposal](https://github.com/tc39/proposal-signals?ref=angularspace.com) to support signals natively in JavaScript. Most of the new Angular APIs, like the Resource API, are designed to work with signals. Signals also simplify state management, since you can both listen to changes and synchronously read their current value.”* ### Can you explain the diamond problem in Observables and why it doesn't occur with signals? So far, this question has had a very high failure rate, but I like to see how candidates react to a topic they've likely never encountered in an Angular interview. I first came across the diamond problem in the article from [Mike Pearson - I changed my mind. Angular needs a reactive primitive](https://dev.to/this-is-angular/i-changed-my-mind-angular-needs-a-reactive-primitive-n2g?ref=angularspace.com). He argues why RxJS, even if loved, may not be the safest choice for Angular's long term and why SolidJS chose signals. Mike talks about the diamond problem and the following example is heavily inspired from his blogpost. We are specifically curious about the behavior of the `combineLatest` operator. Let's say we have an `effect` that listens to two signals. Signals are synchronous and batched, meaning that if both signals are updated one after another, the effect will still run only once. However, if we use `combineLatest`, it emits every time a dependency changes, resulting in multiple emissions even for the same update cycle. ```typescript export class TestComponent { prop1 = signal('a'); prop2 = signal('b'); constructor() { effect(() => { const prop1 = this.prop1(); const prop2 = this.prop2(); console.log(`Signal: ${prop1} - ${prop2}`); }); combineLatest([ toObservable(this.prop1), toObservable(this.prop2)] ).subscribe(([p1, p2]) => console.log(`Observable: ${[p1} - ${p2}`)); setTimeout(() => { this.prop1.set('one'); this.prop2.set('two'); }, 1000); } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/diamond-problem.png) Diamond Problem - RxJS vs Signals In the console output, you'll see: - The `effect` logs only once, after both values are updated. - The `combineLatest` logs twice, once for each individual update. This is a concrete example of the diamond problem - duplicated or excessive emissions due to shared dependencies in a reactive graph. Signals avoid this problem thanks to their synchronous and batched behavior. I know this may be more of a “gotcha” question, so you could rephrase the question into: “*Why can Observables like `combineLatest` lead to unnecessary emissions, and how do signals prevent that?*” ### When to use `effect` and `untracked` in a signal based application ? Angular docs has a section [use cases for effects](https://angular.dev/guide/signals?ref=angularspace.com#use-cases-for-effects), that talks about when effects should be used. Based on that I expect a response from the candidate something like: > "*Use effect when you have no other alternative. For example when you need to rely on a reactive value and the other end isn't reactive. Use cases may include DOM API synchronizations, sending data into analytics, communication with a non-reactive library*" It's also important that the candidate has an understanding why the `untracked` function is needed when we want to [remove dependency tracking](https://angular.dev/guide/signals?ref=angularspace.com#reading-without-tracking-dependencies) in an `effect`. Problems I personally encountered many times were that an effect was reading multiple signals, but also modifying some, therefore it created an infinite cycle and was running all the time. Personally, I use `untracked` most of the time, leaving only the dependency signals outside of it. In the following example I want to focus on the input element when the button is clicked. I use [afterRenderEffect](https://angular.dev/api/core/afterRenderEffect?ref=angularspace.com) which works similarly as `effect`, with the key difference being that it runs after the application has completed rendering. ```typescript @Component({ selector: 'app-focus-example', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: ` ` }) export class FocusExampleComponent { editMode = signal(false); value = signal('Initial value'); editInput = viewChild('editInput', { read: ElementRef }); constructor() { afterRenderEffect(() => { const editMode = this.editMode(); untracked(() => { if (editMode ) { // read the element reference once, without tracking it const inputRef = this.editInput(); // defer the focus() until after the DOM is updated setTimeout(() => { inputRef?.nativeElement?.focus(); }) } }) }); } } ``` ### Are life-cycle hooks still needed in a fully signal based application ? This question is great for brainstorming with a candidate, whether he understands these hooks and also how signals work. Based on my experience, many life-cycle hooks can be replaced by signals and reactive primitives: - `NgOnInit` (NO) - Mostly replaceable with `constructor` or `effect()`. This hook is traditionally used for initialization logic that depends on resolved inputs, data fetching, or setting up observers. For simpler logic, `constructor()` suffices, while more complex reactive scenarios are better handled with `effect()`. - `NgOnChange` (NO) - Can be replaced with `computed()` or `effect()`, as these can react to changes in `input()` signal dependencies. - `NgAfterViewInit` (NO) - Replaceable with `effect()` to perform updates on DOM elements, using `viewChild()` signal references as dependencies. - `NgAfterContentInit` (NO) - Similar to `NgAfterViewInit`, `effect()` can handle initialization logic based on `contentChild()` signal references or you can use the [afterNextRender](https://angular.dev/api/core/afterNextRender?ref=angularspace.com) callback. - `NgAfterContentChecked` / `NgAfterViewchecked` (NO) - are called after every change detection cycle, which makes them performance sensitive. You can replace them with [afterRenderEffect](https://angular.dev/api/core/afterRenderEffect?ref=angularspace.com) which runs after the view has been rendered and a signal dependency changed. - `NgOnDestroy` (NO) - For cleanup tasks such as unsubscribing from third-party libraries, clearing intervals, or other manual teardown logic that signals don't automatically handle you can [inject DestroyRef](https://angular.dev/guide/components/lifecycle?ref=angularspace.com#destroyref) and listen on `onDestroy` for this purpose. ## Angular Questions - RxJS Even in a fully signal based application, there are still use cases where RxJS is a better alternative. From my experience, nearly everything can be implemented by signals, but RxJS sometimes offers a more declarative or composable approach, especially when dealing with complex async workflows therefore I open a debate about some RxJS topics. ### What is a higher-order observable and how they differ ? For an indepth reading about this topic I include a blogpost that I've made in the past [Angular Interview: What is a Higher-Order Observable?](https://dev.to/krivanek06/angular-interview-what-is-higher-order-observable-2k03?ref=angularspace.com). A good example is the classic search box use case, where on each keystroke, an API request is made. The candidate should be able to explain how the behavior changes depending on which higher-order observable operator is used. ```typescript export class TestComponent { private readonly http = inject(HttpClient); readonly control = new FormControl(''); search$ = this.control.valueChanges.pipe( // switchMap, concatMap, mergeMap, exhaustMap switchMap((val) => this.http.get('...', { val: val })) ) } ``` - `switchMap` \- cancels any ongoing request when a new value is typed. Ideal for search boxes, only the latest input matters. - `mergeMap` \- triggers all requests in parallel. Every keystroke results in a request, regardless of timing. Good for logging, but not ideal for searches. - `concatMap` \- queues each request and processes them sequentially, preserving order. Better for form submission flows, not live search. - `exhaustMap` \- ignores new values while a request is in progress. Useful to prevent duplicate requests (e.g., during button mashing), but bad for fast-typing search boxes. If you don't use the `abortSignal` for the [resource API](https://angular.dev/guide/signals/resource?ref=angularspace.com), it works as `exhaustMap`. ### What is the difference between `share()` and `shareReplay()` ? Too much of a theoretical question? Not at all. In a legacy project, which still mainly relies on Observables, you may have situations where you use one of them to multicast values to subscribers. There are, however, occasional bugs, for example, when you navigate back and forth between pages. The next time you come back, you no longer have the current value, or you just retriggered logic that should have been cached by the `shareReplay()` operator. Or, you just ignore both and use `BehaviorSubject`. Both `share()` and `shareReplay()` are RxJS multicasting operators. They allow multiple subscribers to share the same source observable, preventing duplicated side effects (like HTTP requests). - Use `share()` when you only want future subscribers to receive emissions. It doesn't retain or replay past values. Essentially, it converts a cold observable into a hot one. - Use `shareReplay()` when you want new subscribers to immediately receive the latest emitted value(s). It's useful for caching scenarios where re-executing the source (e.g., an HTTP request) is costly or undesirable. You can configure `shareReplay()` using options like: - `bufferSize` – The number of previous values to remember and replay to new subscribers. Typically set to `1` for simple caching. - `refCount` – When `true`, the observable automatically unsubscribes from the source when there are no subscribers. When `false`, it stays connected indefinitely (useful for shared streams). ### What does this code do ? - `scan()` \+ `expand()` Both `scan()` and `expand()` are rarely used in everyday Angular development. However, their presence can indicate that a candidate has encountered more complex problems, problems that go beyond the usual use of `map`, `filter`, or `take` operators. I like to show a practical example like this: ```typescript private paginationOffset$ = new Subject(); loadedMessages = toSignal(this.paginationOffset$.pipe( startWith(0), exhaustMap((offset) => this.api.getMessages(offset).pipe( expand((_, i) => (i < 2 ? this.api.getMessages(offset + 20) : EMPTY) ), map((data) => ({ data })), catchError((err) => of({ data: [] })), startWith({ data: [] }), ), ), scan( (acc, curr) => ({ data: [...acc.data, ...curr.data] }), { data: [] as MessageChat[] }, ), ), {initialValue: [] }); nextScroll() { this.paginationOffset$.next(this.loadedMessages().data.length); } ``` The code above represents a recursive API based pagination pattern. Every time the user triggers `nextScroll()` (for example, by clicking a "Load More" button), the number of already loaded messages is emitted into the `paginationOffset$` subject. Inside the `loadedMessages` signal: - `exhaustMap` ignores new emissions until the current inner observable completes. The user is unable to load more data until the first batch completes. - `expand` is used to recursively call the API multiple times. We assume each call returns 20 messages. Using `expand`, we can simulate loading three pages in one shot (initial call + two recursive calls). - `scan` accumulates all the loaded messages into one stream without losing previously fetched ones. - `catchError` ensures that any failed API call doesn't break the chain. - `startWith` ensures the stream emits an initial empty state, avoiding undefined references. ## Summary Other notable questions may be: - Describe a time you had to refactor legacy code in Angular, how did you approach it? - How do you handle code scalability and performance in large Angular apps? - What is OnPush change detection and when would you use it? - What's the difference between `combineLatest`, `withLatestFrom`, and `forkJoin` and how would you decide which to use? - What is you approach to testing, what mocking library do you use? - How would you migrate an existing app to standalone components and Signals gradually? - What is hydration, how you enable it, why is it needed? Overall, these are the types of questions I lean toward. However, the most important one we always ask at the end is: > *“Can you tell us about some more complex feature you have worked on in the past 1–2 years? What was the problem, and how did you solve it?”* The candidate may be missing some Angular specific knowledge, but they may have faced challenging problems, possibly even ones we're currently facing. Prior experience solving real problems often outweighs deep framework trivia, which can be learned over time. Ultimately, it depends on your company's needs. Do you need a strong Angular expert who can refactor and migrate legacy code while minimizing future tech debt? Or are you looking for someone to join a broader team, where they'll grow with support from others? Decide for yourself. Feel free to share your thoughts, catch more of my articles on [dev.to](https://dev.to/krivanek06?ref=angularspace.com), connect with me on [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com) or check my [Personal Website](https://eduardkrivanek.com/?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/Screenshot-2024-11-13-at-15.52.00--1---3---1---1---1-_upscayl_2x_high-fidelity-4x.jpg) ### 6 CSS Snippets Every Front-End Developer Should Know In 2025 URL: https://www.angularspace.com/6-css-snippets-every-front-end-developer-should-know-in-2025/ Last updated: 2025-07-25T19:06:05.000Z 2025; I think every front-end developer should know how to enable page transitions, transition a ``, popover, and `
`, animate light n' dark gradient text, type safe their CSS system, and add springy easing to animation. ### **AI is not going to give you this CSS.** This post is a theme continuation; checkout previous years [2023](https://web.dev/articles/6-css-snippets-every-front-end-developer-should-know-in-2023?ref=angularspace.com) and [2024](https://web.dev/articles/5-css-snippets-every-front-end-developer-should-know-in-2024?ref=angularspace.com) where I shared snippets for those years. This year, the snippets are bigger, more powerful, and leverage progressive enhancement a bit more; to help us step up to the **vast UI/UX requirements of 2025**. ## Springy easing with `linear()` [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2025-01-23-at-18.29.42.png)](https://github.com/web-platform-dx/web-features/blob/main/features/linear-easing.yml?ref=angularspace.com) Sprinkle life into animations with natural looking spring and bounce easings using `linear()`. 0:00 /0:23 1× Using a series of linear lines to make "curves", you can create surprisingly realistic visual physics. **A small amount can go a long way** to adding [interest and intrigue](https://codepen.io/argyleink/pen/PoxgOZz?ref=angularspace.com) to the user experience. In the following video, the top animation uses `ease-out` and the bottom uses `linear()`, and I think the results are quite different, the bottom being more desirable. 0:00 /0:18 1× Here's some typical `linear()` easing CSS 😅: ```css .springy { transition: transform 1s linear( 0, 0.009, 0.035 2.1%, 0.141, 0.281 6.7%, 0.723 12.9%, 0.938 16.7%, 1.017, 1.077, 1.121, 1.149 24.3%, 1.159, 1.163, 1.161, 1.154 29.9%, 1.129 32.8%, 1.051 39.6%, 1.017 43.1%, 0.991, 0.977 51%, 0.974 53.8%, 0.975 57.1%, 0.997 69.8%, 1.003 76.9%, 1.004 83.8%, 1 ); } ``` Yes… that's what your formatter will do to it, as if it's helpful in some way lol. The `linear()` code above is not very human readable, but the machines love it. No frets, there's a few generators out there: - [https://linear-easing-generator.netlify.app](https://linear-easing-generator.netlify.app/?ref=angularspace.com) - [https://easingwizard.com](https://easingwizard.com/?ref=angularspace.com) ### **Tip! 💡** > Expect longer durations when using `linear()`. When things run long, it can be nice to make them seamlessly interruptible, making `linear()` a great fit for [transitions](https://developer.mozilla.org/docs/Web/CSS/transition?ref=angularspace.com) and potentially troublesome as keyframes. You could alternatively use premade CSS variables from a library like [Open Props](https://open-props.style/?ref=angularspace.com#easing): ```css @import ""; .springy { @media (prefers-reduced-motion: no-preference) { transition: transform 1s var(--ease-spring-3); } } ``` Easy CSS to read, comes with 5 strengths for common effects: 0:00 /0:01 1× See the Pen [Open Props - Pop In animation](https://codepen.io/argyleink/pen/ZEXQovz?ref=angularspace.com) by Adam Argyle ([@argyleink](https://codepen.io/argyleink?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). ### Tip💡 > [Try the **Open Props Springs** notebook!](https://nerdy.dev/notebook/open-props-springs.html?ref=angularspace.com) <-- ### Incrementally adopt This one is super easy to toss in today. Easiest way, use the cascade (if you're into that): ```css @media (prefers-reduced-motion: no-preference) { /* just repeat the shorthand with adjusted easing */ .thingy { transition: transform 0.3s ease; transition: transform 0.3s linear(…); } /* or, target a specific property */ .thingy { animation-timing-function: var(--ease-1); animation-timing-function: var(--ease-spring-2); } } ``` **If it knows, it knows.** Or, test for it first and scalpel apply the upgrade: ```css .thingy { transition: transform 0.3s ease; @supports (transition-timing-function: linear(0, 0.1, 1)) { transition-timing-function: var(--ease-spring-2); } } ``` ## Typed custom properties [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2025-01-23-at-18.31.07.png)](https://github.com/web-platform-dx/web-features/blob/main/features/registered-custom-properties.yml?ref=angularspace.com) Similar to JS variables defined with `var`, CSS variables defined with `--` are global, loose, dynamic and flexible. This is great. But… there are times, **like when building a system**, that you want to limit what goes into variables so a system can run with a reasonable amount of reliability. 0:00 /0:35 1× See the Pen [Epic Web Conf - @property typesafe](https://codepen.io/argyleink/pen/qBwKmNe?ref=angularspace.com) by Adam Argyle ([@argyleink](https://codepen.io/argyleink?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). In the above video, a variable is set to an invalid color. At first, this breaks the system. But, [@property](https://web.dev/blog/at-property-baseline?ref=angularspace.com) is added, the system remained functioning with the latest known valid color value. Create a typed `` CSS variable like this: ```css @property --color-1 { syntax: ""; inherits: false; initial-value: rebeccapurple; } ``` In addition to type safety, `@property` defined variables can also be animated because the browser can infer the steps needed to interpolate the value change based on the assigned type. Before `@property`, the browser couldn’t derive a type and discover interpolation steps, it was too complicated. Now, you give the browser a hint, and it's simple. In 2025, y'all front-end devs should be getting familiar with defining variables with `@property` because it: 1. Formalizes CSS system interfaces 2. Protects CSS systems 3. Enables new animation powers 4. Can [perform better](https://web.dev/blog/at-property-performance?ref=angularspace.com) when using `inherits: false` Resources - [https://codepen.io/argyleink/pen/vYPdBOO](https://codepen.io/argyleink/pen/vYPdBOO?ref=angularspace.com) - [https://www.youtube.com/watch?v=tSfSY3Ni3X0&t=3409s](https://www.youtube.com/watch?v=tSfSY3Ni3X0&t=3409s&ref=angularspace.com) - [https://nerdy.dev/cant-break-this-design-system](https://nerdy.dev/cant-break-this-design-system?ref=angularspace.com) - [https://nerdy.dev/type-guarded-css-systems-with-@property](https://nerdy.dev/type-guarded-css-systems-with-@property?ref=angularspace.com) - [https://www.epicweb.dev/talks/lightning-in-a-bottle-with-css-custom-properties](https://www.epicweb.dev/talks/lightning-in-a-bottle-with-css-custom-properties?ref=angularspace.com) - [https://codepen.io/argyleink/pen/MWZMxNN](https://codepen.io/argyleink/pen/MWZMxNN?ref=angularspace.com) ## View transitions for page navigation Y'all should know how to crossfade pages when links are clicked with this tiny view transitions snippet: ```css @view-transition { navigation: auto; } ``` 0:00 /0:21 1× [https://codepen.io/argyleink/project/full/DezgjV](https://codepen.io/argyleink/project/full/DezgjV?ref=angularspace.com) **This is the easiest snippet to add to your site, with no downsides.** It signals your website would like to use page transitions when links are clicked, and the default transition is a crossfade. If the browser doesn't support it, it continues as it always has; but if it does support it then you dab the page with some special sauce. There's plenty more customization you can do, like [full page animations](https://codepen.io/argyleink/project/editor/ZBnpqB?ref=angularspace.com). But the gist of this section is just to share that easy snippet and the way the feature can be progressively enhanced. ### Incrementally adopt There's many more opportunities to add additional animations with the page transition. A great place to start enhancing this page transition experience is to identify elements commonly found across pages, and give them a `name`: ``` .nav { view-transition-name: --nav; } .sidenav { view-transition-name: --sidenav; } ``` **This includes elements in the page transition.** They can even be different elements. ``` a, h1 { view-transition-name: --morphy; } ``` By giving an `` and an `

` the same [view-transition-name](https://developer.mozilla.org/en-US/docs/Web/CSS/view-transition-name?ref=angularspace.com) on two different pages, the browser will move and resize the page 1 element to the location and size of the page 2 element, making it look like a morph. You can of course morph between the same elements also. 0:00 /0:18 1× [https://codepen.io/argyleink/project/full/AbvgrM](https://codepen.io/argyleink/project/full/AbvgrM?ref=angularspace.com) There is so much more. Continue giving elements names and studying the [rad examples by Bramus](https://view-transitions.chrome.dev/?ref=angularspace.com), and you can create experiences with motion like this: 0:00 /0:37 1× [https://view-transitions.chrome.dev/off-the-beaten-path/mpa/](https://view-transitions.chrome.dev/off-the-beaten-path/mpa/?ref=angularspace.com) I love the [DevTools for Animations](https://developer.chrome.com/docs/devtools/css/animations?ref=angularspace.com), scrubbing that full page view transition is very satisfying, and excellent for really inspecting and improving the little details. Resources - [Multi-page application View Transitions are here](https://www.youtube.com/watch?v=eY6C%5F-aDdTo&ref=angularspace.com) - [Modern CSS for Sites Workshop](https://www.youtube.com/watch?v=oXSFwix7eR8&ref=angularspace.com) - [The CSS Podcast - #89 on View Transitions](https://www.youtube.com/watch?v=xBEvOh9jlis&ref=angularspace.com) - [Full page transitions starter](https://codepen.io/argyleink/project/editor/ZBnpqB?ref=angularspace.com) - [Bramus with a kit for IE Page-like Transitions](https://github.com/bramus/ie-page-transitions?ref=angularspace.com) ## Transition animation for `` and `[popover]` In 2025, knowing your way around a [](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog?ref=angularspace.com) and a [\[popover\]](https://developer.mozilla.org/en-US/docs/Web/HTML/Global%5Fattributes/popover?ref=angularspace.com) are table stakes. Otherwise, everyone else will be on top of you, and your wack `z-index` attempts will be defeated with a puny value of `1`. These are common UI elements, with no JavaScript to download, and accessibility built in. **Use em** and [know the differences](https://hidde.blog/dialog-modal-popover-differences/?ref=angularspace.com). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/image-8.png) These two elements are projected into a layer above all the other UI called the [top layer](https://developer.chrome.com/blog/what-is-the-top-layer?ref=angularspace.com). The browser projects the elements from anywhere in the document, to the top when shown. **To transition this**, there’s a few new CSS properties, for the full *interruptible* CSS transition user experience — [transition-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior?ref=angularspace.com), the [@starting-style](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style?ref=angularspace.com) rule, and [overlay](https://developer.mozilla.org/en-US/docs/Web/CSS/overlay?ref=angularspace.com). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/image-5.png) Combining these can feel like an incantation, but that makes it great copy and paste. So here! Use the following snippet to enable cross fade transitions for both `` and `popover`: [Try it](https://codepen.io/argyleink/pen/zYbQBOm?ref=angularspace.com) ```css /* enable transitions, allow-discrete, define timing */ [popover], dialog, ::backdrop { transition: display 1s allow-discrete, overlay 1s allow-discrete, opacity 1s; opacity: 0; } /* ON STAGE */ :popover-open, :popover-open::backdrop, [open], [open]::backdrop { opacity: 1; } /* OFF STAGE */ /* starting-style for pre-positioning (enter stage from here) */ @starting-style { :popover-open, :popover-open::backdrop, [open], [open]::backdrop { opacity: 0; } } ``` While this code is effective and terse, it's often not enough customization if you want to present and dismiss dialogs differently than you do popovers. Or make the entry animation different then the exit. ### Transition a dialog Here's a `` element with this barebones snippet applied. They look pretty terrible out of the box, but you can [do amazing things with them](https://nerdy.dev/have-a-dialog?ref=angularspace.com). 0:00 /0:07 1× See the Pen [pop-n-lock basic - transitioned](https://codepen.io/argyleink/pen/OJeWWNZ?ref=angularspace.com) by Adam Argyle ([@argyleink](https://codepen.io/argyleink?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). To get started, a `` element needs to be in the HTML. A `` should be shown and hidden with buttons, one to show it can be in the page and one to close it should be inside the dialog. ```
``` You can enable **light dismiss** on a dialog and skip the close button like `` ### Tip💡 > You can enable **light dismiss** on a dialog and skip the close button like `` To animate the dialog transition: 1. Two parts need animation: the `` itself and its [::backdrop](https://developer.mozilla.org/en-US/docs/Web/CSS/::backdrop?ref=angularspace.com). 2. When a dialog is shown, `display: none` is changed to `display: block` and [transition-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior?ref=angularspace.com) enables timing this change with our animation. 3. When a dialog is shown, it is cloned into the [top layer](https://developer.mozilla.org/en-US/docs/Glossary/Top%5Flayer?ref=angularspace.com), this also needs to be timed with our animation. 4. The [\[open\]](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/open?ref=angularspace.com) attribute is used to know when the dialog is open or closed. [@starting-style](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style?ref=angularspace.com) is used during the first render as starting styles. ```css dialog { /* Exit Stage To */ transform: translateY(-20px); &, &::backdrop { transition: display 1s allow-discrete, overlay 1s allow-discrete, opacity 1s ease, transform 1s ease; /* Exit Stage To */ opacity: 0; } /* On Stage */ &[open] { opacity: 1; transform: translateY(0px); &::backdrop { opacity: 0.8; } } /* Enter Stage From */ @starting-style { &[open], &[open]::backdrop { opacity: 0; } &[open] { transform: translateY(20px); } } } ``` See the Pen [pop-n-lock basic - transitioned](https://codepen.io/argyleink/pen/OJeWWNZ?ref=angularspace.com) by Adam Argyle ([@argyleink](https://codepen.io/argyleink?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). With this snippet as a starting point, you can find three popular dialog experiences for you to take code or inspiration from [have-a-dialog](https://nerdy.dev/have-a-dialog?ref=angularspace.com). The following video shows the excellent keyboard experience. It also demonstrates the interruptible nature of a CSS transition, so a user can close it anytime they want and always see a smooth interface. 0:00 /0:37 1× [nerdy.dev/have-a-dialog](https://nerdy.dev/have-a-dialog?ref=angularspace.com) Resources - [https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement?ref=angularspace.com) - [https://nerdy.dev/notebook/dialog-starter.html](https://nerdy.dev/notebook/dialog-starter.html?ref=angularspace.com) - [https://www.youtube.com/watch?v=yORy1IEHDKk](https://www.youtube.com/watch?v=yORy1IEHDKk&ref=angularspace.com) - [https://web.dev/blog/baseline-entry-animations](https://web.dev/blog/baseline-entry-animations?ref=angularspace.com) - [Pop n' Lock Dialog Mini Web Machine](https://www.youtube.com/watch?v=f3N-6MzK8Z0&list=PLNYkxOF6rcIDCWoS%5FGSIwA24gZcwtLCZj&index=1&ref=angularspace.com) ### Transition a popover Like a `` element, a popover appears over everything else in the top layer. Light dismiss is the default, and keyboard / focus management is all done for you. 0:00 /0:09 1× See the Pen [Toggle Tip - Anchor + Popover - Over-Easy Mini Web Machine](https://codepen.io/argyleink/pen/mdZXzxW?ref=angularspace.com) by Adam Argyle ([@argyleink](https://codepen.io/argyleink?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). **Let's bulid it.** There's an HTML aspect to implementing the UX: ```

An overlay with additional information.

``` Also, like a `` element, to animate the transition of a popover's display property and insertion into the top layer, you need to combine `transition-behavior` and `@starting-style`. Notice with a popover, the open state isn't an attribute, it's a css pseudo-class `:popover-open`. ```css [popover] { &, &::backdrop { transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete, opacity 0.5s, transform 0.5s; /* Exit Stage To */ opacity: 0; } /* On Stage */ &:popover-open { opacity: 1; &::backdrop { opacity: 0.5; } } /* Enter Stage From */ @starting-style { &:popover-open, &:popover-open::backdrop { opacity: 0; } &:popover-open { transform: translateY(10px); } } } ``` See the Pen [Transition popover in and out](https://codepen.io/argyleink/pen/JjzqXee?ref=angularspace.com) by Adam Argyle ([@argyleink](https://codepen.io/argyleink?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). Resources - [https://nerdy.dev/steal-this-popover-starter-kit](https://nerdy.dev/steal-this-popover-starter-kit?ref=angularspace.com) - [https://nerdy.dev/notebook/dialog-starter.html](https://nerdy.dev/notebook/dialog-starter.html?ref=angularspace.com) - [https://developer.chrome.com/blog/entry-exit-animations](https://developer.chrome.com/blog/entry-exit-animations?ref=angularspace.com) - [https://developer.chrome.com/blog/css-ui-ecommerce-popover](https://developer.chrome.com/blog/css-ui-ecommerce-popover?ref=angularspace.com) - [Over-Easy Anchor + Popover Mini Web Machine](https://www.youtube.com/watch?v=ASb9vO3ARHo&list=PLNYkxOF6rcIDCWoS%5FGSIwA24gZcwtLCZj&index=5&ref=angularspace.com) ## Transition animation for `
` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/image-6.png) Found on the [CSS Wrapped 2024](https://chrome.dev/css-wrapped-2024/?ref=angularspace.com) website in the desktop layout. 0:00 /0:06 1× The disclosure element (`
`) has been waiting for CSS primitives to unlock its animation potential for many years. ```
Show disclosed content

``` The details element needs to transition to `height: auto` from `height: 0px` and a way to target the slotted content it internally uses for the disclosure. The new [interpolate-size](https://developer.mozilla.org/en-US/docs/Web/CSS/interpolate-size?ref=angularspace.com) feature can be used for the height animation and [::details-content](https://developer.chrome.com/blog/styling-details?ref=angularspace.com) for the selector. ```css details { inline-size: 50ch; @media (prefers-reduced-motion: no-preference) { interpolate-size: allow-keywords; } &::details-content { opacity: 0; block-size: 0; overflow-y: clip; transition: content-visibility 1s allow-discrete, opacity 1s, block-size 1s; } &[open]::details-content { opacity: 1; block-size: auto; } } ``` See the Pen [
open/close transitions](https://codepen.io/argyleink/pen/QWewVjd?ref=angularspace.com) by Adam Argyle ([@argyleink](https://codepen.io/argyleink?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). Resources - [https://developer.chrome.com/blog/styling-details](https://developer.chrome.com/blog/styling-details?ref=angularspace.com) - [https://nerdy.dev/open-and-close-transitions-for-the-details-element](https://nerdy.dev/open-and-close-transitions-for-the-details-element?ref=angularspace.com) - [https://css-tricks.com/almanac/pseudo-selectors/d/details-content/](https://css-tricks.com/almanac/pseudo-selectors/d/details-content/?ref=angularspace.com) ### Bonus attribute ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/image-7.png) If you want to connect two or more details elements and have them close each other respectively, you can accomplish this with a shared [name](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details?ref=angularspace.com#name) attribute on each detail element you want to be connected. Very much like a [radio group](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio?ref=angularspace.com). 0:00 /0:13 1× [https://developer.chrome.com/blog/styling-details](https://developer.chrome.com/blog/styling-details?ref=angularspace.com) ```html
Show disclosed content

"; inherits: false; initial-value: #000; } @property --color-2 { syntax: ""; inherits: false; initial-value: #000; } @keyframes color-change { to { --color-1: var(--_color-1-to); --color-2: var(--_color-2-to); } } .gradient-text { --_space: ; /* light mode */ --_color-1-from: yellow; --_color-1-to: orange; --_color-2-from: purple; --_color-2-to: hotpink; /* dark mode */ @media (prefers-color-scheme: dark) { --_color-1-from: lime; --_color-1-to: cyan; --_color-2-from: cyan; --_color-2-to: deeppink; } --color-1: var(--_color-1-from); --color-2: var(--_color-2-from); animation: color-change 2s linear infinite alternate; background: linear-gradient( to right var(--_space), var(--color-1), var(--color-2) ); /* old browser support */ -webkit-background-clip: text; -webkit-text-fill-color: transparent; /* modern browser version */ background-clip: text; color: transparent; @supports (background: linear-gradient(in oklch, #fff, #fff)) { --_space: in oklch; } } ``` That's quite a snippet 😅 How did it get to that? Most developers making a gradient text effect are starting here: ``` .gradient-text { background: linear-gradient(to right, hotpink, cyan); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } ``` ### remove the prefixes The first update or enhancement is to remove the prefixes. Although, so older browsers continue to support the effect, add the unprefixed values after: ```css .gradient-text { background: linear-gradient(to right, hotpink, cyan); /* old browser support */ -webkit-background-clip: text; -webkit-text-fill-color: transparent; /* modern browser version */ background-clip: text; color: transparent; } ``` ### Use updated gradient interpolation spaces Next, improve the quality of the gradient by progressively enhancing the `in` [interpolation syntax](https://developer.chrome.com/docs/css-ui/access-colors-spaces?ref=angularspace.com#color%5Finterpolation) with CSS variables and `@supports`. You could alternatively repeat the gradient definition and include `in oklch` in it, which would also work great and support older browsers. ```css .gradient-text { --_space: ; background: linear-gradient(to right var(--_space), hotpink, cyan); /* old browser support */ -webkit-background-clip: text; -webkit-text-fill-color: transparent; /* modern browser version */ background-clip: text; color: transparent; @supports (background: linear-gradient(in oklch, #fff, #fff)) { --_space: in oklch; } } ``` ### Create typed `` properties For the gradient color animation use `@property`, like described in snippet #4\. The typed color values can be animated inside of a gradient, like a gradient used with text. ```css @property --color-1 { syntax: ""; inherits: false; initial-value: #000; } @property --color-2 { syntax: ""; inherits: false; initial-value: #000; } ``` Now `--color-1` can be animated like `transition: –color-1 .3s ease` or used in keyframes. These values that can animate, can be used anywhere color is allowed, like in a gradient text effect. ```css @keyframes color-change { to { --color-1: lime --color-2: orange; } } .gradient-text { animation: color-change 2s linear infinite alternate; } ``` ### Make a few props, Swap em' in a dark MQ To keep things declarative, I've also defined color variables to hold the colors for animation. ```css .gradient-text { --_color-1-from: yellow; --_color-1-to: orange; --_color-2-from: purple; --_color-2-to: hotpink; @media (prefers-color-scheme: dark) { --_color-1-from: lime; --_color-1-to: cyan; --_color-2-from: cyan; --_color-2-to: deeppink; } /* set our typed variables to the "from" values */ --color-1: var(--_color-1-from); --color-2: var(--_color-2-from); } ``` Put all those moments and reasons together, and we have arrived at the final snippet: ```css @property --color-1 { syntax: ""; inherits: false; initial-value: #000; } @property --color-2 { syntax: ""; inherits: false; initial-value: #000; } @keyframes color-change { to { --color-1: var(--_color-1-to); --color-2: var(--_color-2-to); } } .gradient-text { --_space: ; /* light mode */ --_color-1-from: yellow; --_color-1-to: orange; --_color-2-from: purple; --_color-2-to: hotpink; /* dark mode */ @media (prefers-color-scheme: dark) { --_color-1-from: lime; --_color-1-to: cyan; --_color-2-from: cyan; --_color-2-to: deeppink; } --color-1: var(--_color-1-from); --color-2: var(--_color-2-from); animation: color-change 2s linear infinite alternate; background: linear-gradient( to right var(--_space), var(--color-1), var(--color-2) ); /* old browser support */ -webkit-background-clip: text; -webkit-text-fill-color: transparent; /* modern browser version */ background-clip: text; color: transparent; @supports (background: linear-gradient(in oklch, #fff, #fff)) { --_space: in oklch; } } ``` Some years these snippets are short and sweet, but not this year. Watch out for next year's article, who knows what you'll need to know! --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/Screenshot-2024-11-13-at-15.52.00--1--1.jpg) ### New Angular Space Advisor! URL: https://www.angularspace.com/new-angular-space-advisor/ Last updated: 2025-07-18T09:21:44.000Z Angular Space vision is growing bigger everyday. With aim to create truly unique experience. To make sure I can provide the highest quality going forward I decided to bring new Advisor to join our ranks! ## **Welcome** **Gregor Ojstersek**! --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/Screenshot-2025-07-17-at-18.23.07.png)](https://maven.com/gregor-ojstersek/senior-engineer-to-lead?ref=angularspace.com) **Gregor Ojstersek** is a CTO who founded [Engineering Leadership newsletter ](https://newsletter.eng-leadership.com/?ref=angularspace.com)(150k+ subscribers). We already had **2x Giveaways** of his amazing [workshops](https://maven.com/gregor-ojstersek/senior-engineer-to-lead?ref=angularspace.com) that are teaching a much needed mindset switch for everyone aspiring to be a leader & existing leaders on how to improve in the role. His expert insights and ability to execute are going to bring me much needed help crafting Angular Space going forward. Advisors are here to help me decide how to proceed with Angular Space growth & development. A Group of friendly people to brainstorm ideas with. ### Certificates.dev Review: Mid-level Angular Developer URL: https://www.angularspace.com/certificates-dev-review-mid-level-angular-developer/ Last updated: 2025-07-16T14:03:36.000Z Although Angular is regarded as an "enterprise framework," developers often struggle to prove their knowledge without practical tasks. Of course, there's the "Google Developer Experts" program, but some of its requirements might deter potential experts, and it doesn’t focus solely on Angular - Instead, it promotes Google technologies such as cloud solutions, Firebase, Android, and AI. This is where platforms like [Certificates.dev](https://certificates.dev/angular?friend=ANGSPACE20&ref=angularspace.com) come in—offering certification programs specifically focused on frontend technologies, including Angular! Thanks to the platform’s authors and AngularSpace, I had the opportunity to go through the preparation process and certification for the Mid-Level Angular Developer. Thank you! ### Let's start at the beginning - Who Am I? I’m Adam, a developer with over a decade of experience. By profession, I am a Fullstack Developer—comfortable with both frontend and backend — but my passion lies in Angular, which I fell in love with from the first line of code. I worked extensively with AngularJS 1.3 (yes, really!), I remember the monumental release of Angular 2.0, and now I closely follow every new Angular release with excitement. Angular is my primary tool for work and my first choice for new web projects. Yet, I don't feel entirely confident in my Angular knowledge. There are some concepts I understand in my own way, and others I need further explanation on. [Certificates.dev](https://certificates.dev/angular?friend=ANGSPACE20&ref=angularspace.com) offers a training program and certification for developers, not only at the Mid-Level but also at Junior and Senior levels! Beyond Angular certifications, the platform also provides certification paths for Vue.js, JavaScript, and Nuxt, with upcoming courses for React, TypeScript, and even Tailwind! All certifications are created in collaboration with experts and platform creators, making it a comprehensive offer worth considering! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0001_page_present.gif) ### Day 0: First look at panel My first impression after logging in was very positive! The dashboard’s color scheme references Angular’s rebranding. The interface is clear: on the left, there’s a panel with available functions and user information, while on the right, descriptions of available actions. The first thought that crossed my mind was, “It’s possible to create a great looking Angular themed page without using Material Design, right?” ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0002_dashboard.png) I was also pleasantly surprised by the mobile view. When I'm choosing a course, it's important for me to know which platforms I can access it from. Just like with news, sometimes I read on a larger screen during a work break, while at other times, I want to glance through something while I'm on the road (as a passenger, of course!) or before sleep. Surprisingly, the site is highly responsive. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0003_mobile_view_dashboard.png) ### First day of training, and another days... I wasn’t confident enough to take the full exam right away, so the next evening, I decided to go through some training lessons. The training section follows the same dashboard theme, with progress tracking on the left and main content on the right—everything remains clear and responsive. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0005_training_start.png) What surprised me was the format of the lessons. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0007_how_training_looks.png) Above, you can see an example chapter dedicated to Signals in Angular. This is a completely different approach compared to other courses I’ve encountered. I expected long-form articles or perhaps videos explaining the topic. Instead, we get a brief description of the lesson topic and a set of links to relevant resources. At first, I was a bit confused by this approach. Some Angular topics are so complex that entire conference talks are dedicated to them—yet here, we only get a summary and a list of links. Does this mean the training is incomplete? Actually, no! It took me some time to get used to this format. The links provided in the lessons mostly lead to the official Angular documentation, blog posts from Angular Training, or Medium articles written by Alain Chautard under the Angular Training brand. This is an interesting way to present training material: rather than lengthy lessons, the platform provides a knowledge base—concise, high-quality content that can be read in just a few minutes. This is how an example article looks, it's about "Standalone Components": ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0006_example_of_links.png) One might ask, "If these links are publicly available, why should I pay for someone who aggregates them into chapters?" My answer is simple: time-saving. The internet is full of articles of varying quality. In my experience, most of them are written as tutorials, where before getting to the core topic, you must wade through installation instructions for Node.js, Angular CLI, project setup, and library installations. Here, the provided content is curated by Google Developer Experts — free from unnecessary fluff focused on the topic. The authors assume you already have some knowledge, but a few paragraphs can refine and deepen your understanding. However, this approach does have a drawback: taking this course on a mobile device becomes inconvenient. While I tried to complete lessons during breaks, opening and closing multiple browser tabs wasn’t comfortable. In the end, I had to finish certain lessons on my desktop. From time to time, quizzes also appear! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0008_quiz.png) The quiz questions are relevant and refer to the topics covered in the chapter. I didn’t notice any mistakes, and they serve as a great way to reinforce learning. Additionally, the course includes several challenges where you download a project and modify it according to given requirements. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0009_challange.png) However, I missed one crucial feature — a way to verify if my implementation was correct. I understand that developing an automated grading system is time consuming and costly, implementing unit tests could be a solution. Instead of manually comparing projects, users could write code, test it in a browser, and run unit tests to check their work. Completing the training took me a few days, spending about an hour daily reading articles, solving quizzes, and completing programming tasks. I believe a highly motivated person could finish it in just one day! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0010_finished-training.png) What was missing? It seems to me that a person at the “Mid” level should have a basic understanding of `@ngrx/store`, I missed a dedicated lesson that could describe this - even as an "add-on" that would not be included in the exam. I would have also supplemented chapter five on Dependency Injection, mentioned InjectionToken, "factory" in providers for component, module, and application. This was not present, perhaps these are advanced topics, but such knowledge is very welcome. ### It's time to Exam! After completing the entire training, we have the opportunity to take a "trial exam." This is a shortened version of what we can expect in the main exam—fewer questions, simpler coding tasks, and less time to complete them. Honestly, after the training, I felt confident and went straight to the main exam. The moment I started it, my heart began to race... The exam takes itself very seriously. At the beginning, we are instructed on how the test works and how we should prepare for it. We are required to turn on our webcam and microphone, turn off our phone, and disconnect any additional screens from our device if applicable. External software verifies our identity, asks us to show our desk to check for any cheating attempts, and throughout the exam, we are continuously monitored — our webcam, microphone, and screen are recorded. These verification methods are not used for the trial exam, so for someone experiencing this for the first time, it can be nerve-wracking! The exam is divided in two parts. The first part consists of a test with 40 closed questions. The questions are, of course, related to the knowledge gained during the training, primarily focusing on Angular. However, in my case, there were also a few questions related to JavaScript/TypeScript that required some thought. This section has a time limit of 30 minutes, and at least 29 correct answers are required to pass. The second part is a practical exam, where we have to complete two tasks. In the first task, we must find and fix a bug in an existing application. The exam creators suggest spending about 15 minutes on this (it took me 25 minutes — shame on me!). The second task involves implementing certain functionalities in an application according to the given requirements. Fortunately, the application already includes templates and necessary data (which we would normally fetch from an API), so we only need to focus on implementing the logic in TypeScript. This part turned out to be quite manageable for me and I finished with 30 minutes to spare (not shame on me!). Before the exam, we have access to a short video explaining how the practical section works, but I’ll describe it for those interested. Before starting, we receive a full description of what is expected from us. We work with an editor using StackBlitz, which includes all the necessary project files. This means we cannot use our preferred IDE, but instead, we get a toolbar with quick access to task requirements, the Angular documentation, and the MDN documentation. Clicking on these resources opens them in a "SideNav," which is a very well thought out solution. It keeps the code in view, prevents cheating, and best of all, does not require us to learn the entire documentation to pass the exam. If needed, we can quickly reference it during the exam. In my case, I had to use one of Angular’s built-in Pipes, which I had never used before. A quick look at the documentation helped me understand its parameters and implement the required functionality! Once the exam ends, we are no longer tracked. I breathed a sigh of relief when I saw the message: "Thank you for completing the exam." The same message informed me that the grading process would take about five business days. However, the next morning, I checked my email and saw a message from Certificates.dev that... ### I'm Certified Mid-Level Angular Developer! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/0011_certificate_achived.png) The certificate can be downloaded, shared on social media, and added to the "Licenses & Certifications" section of your LinkedIn profile to earn some likes from colleagues. ### Summary Would I recommend this platform for learning and certifying Angular skills? Absolutely! The training provides a solid dose of knowledge along with high quality articles from experts who truly know that stuff. The Mid-Level certification is well-balanced, and it covers exactly the topics I would expect to discuss with another "Mid-Level developer". Once again, I would like to thank AngularSpace for the opportunity to test and verify my knowledge, but most importantly, a huge thanks to the creators of Certificates.dev — this platform is a remarkable achievement with a unique yet effective approach to delivering and validating knowledge. I hope that the certification I obtained will give me more confidence when discussing Angular topics. I will keep an eye on this platform and will certainly take additional lessons from it. I'm rooting for the continued development of this platform and looking forward to even more certification options! I have already signed up for the React course, and in the future, I will likely go for the "Senior Angular Developer" training and certification — so see you soon! --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/Screenshot-2025-07-16-at-14.54.10.jpg) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/Screenshot-2024-11-13-at-15--1---1---1---1---1-.jpg) ### From AngularConnect to AngularDisconnect - Giveaway Cancelled URL: https://www.angularspace.com/from-angularconnect-to-angulardisconnect-giveaway-cancelled/ Last updated: 2025-07-15T07:56:47.000Z Hi everyone, I'm really disappointed to share that I need to **cancel the giveaway** for the 90% discounted AngularConnect tickets. This is the **first time something like this has happened**. I’ve organized many giveaways before and they’ve always gone smoothly. Based on the original communication I received from the organizers, there was **no mention** that the discount would be limited to **in-person tickets only**. The promo banner I received (which I thankfully hadn't used) also mentioned that the 90% discount codes should apply to workshops as well. Guess what? They didn't - but I managed to catch that beforehand. Just sounded like too good of a deal since workshops are expensive. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/IMG_2476.jpeg) With **Conference** however - Naturally, I assumed it would work for online tickets as well - just like many similar codes do - and I **would never have expected** this to be an issue. After selecting winners (both of whom chose the online ticket option), I tested the codes only to find out they didn't apply. I reached out for clarification, explained the situation, and even offered to help cover the cost if needed ( Even though Online tickets are cheaper ). Unfortunately, the organizers decided to **stick firmly to their policy** and **refused to make an exception**, despite the miscommunication on their side and the fact that winners were already selected. I respect the right to set terms by Organizers - it's their Conference after all, but I do believe this could have been communicated more clearly from the start and handled with more flexibility. Thank you to everyone who participated in the giveaway, supported it, or looked forward to attending. I hope AngularConnect is going to end up being an amazing conference and wishing them all the best - but in these circumstances Angular Space needs to withdraw from community partnership. I'm truly sorry for the inconvenience, especially to the winners - **I’ll be reaching out to you directly with info about the consolation prize that I have personally prepared.** If anyone has questions, feel free to message me. ### How to get rid of Angular Animations right now URL: https://www.angularspace.com/how-to-get-rid-of-angular-animations-right-now/ Last updated: 2025-09-18T09:03:31.000Z If you were paying attention, the Angular team now [advises against](https://angular.dev/guide/animations?ref=angularspace.com) using the Angular Animations package. It comes as no surprise, since CSS animations are super powerful and Web Animations API is now supported by all browsers. A time when we might need this 60kB extra package is over and the official suggestion is to migrate according to [this guide](https://angular.dev/guide/animations/migration?ref=angularspace.com). Most of the time that would be a walk in the park, however one little case still prevents us from migrating easily. And that is the `:leave` state – performing an animation on element removal from DOM. Angular currently has an [RFC](https://github.com/angular/angular/discussions/62212?ref=angularspace.com) for a way to remedy that. But what if we want to lighten our bundle right now? In [Taiga UI](https://taiga-ui.dev/?ref=angularspace.com), a components library I work on, we implemented a similar approach a few weeks before RFC was published. Let’s see how it works. ## Angular Renderer A place where this is done is the [Renderer](https://angular.dev/api/core/Renderer2?ref=angularspace.com). Angular animations package provides its own [renderer factory](https://angular.dev/api/core/RendererFactory2?ref=angularspace.com) which creates a special renderer for animations, as well as the default one under the hood. I do not say this often, but this is where Angular has a weak spot in terms of customization. While usually we can augment any behavior with DI, in this case it falls short. If you are working on an app, it’s usually enough – you can just remove animations and provide your own renderer factory with the implementation we are about to make. But if you are working on a library you do not have control over this. The app using your library might still rely on animations somewhere or even have its own renderer. So while we wait for the Angular team to add official support for `:leave` state – we will have to hack our way into it and augment renderer behavior on the fly. ## Specifying the task We want the following: 1. Apply `app-enter` class on the element when it is created 2. Remove it after animations play out or if there were no animations 3. Apply `app-leave` class on the element when it is removed and keep it in the DOM 4. Remove it from DOM when animations play out or if there were no animations This is a job for a directive that we will call `appAnimated`. We will also have some global styles to simplify setting it up. And if we need to apply animations to components, we can use it as a host directive. For both host directives overview and adding styles to directives I refer you to my [previous article](https://www.angularspace.com/host-directives-decomposition-unleashed). Here are the styles we want to always be applied to our elements: ```css .app-enter, .app-leave { animation-duration: var(--app-duration); pointer-events: none; } .app-leave { animation-direction: reverse; } ``` All of those rules are easily overridden, but they give us a good baseline, allowing us to set speed and to just specify animation-name and it will be played in both directions properly. Disabling pointer-events on animations is optional, but I recommend doing so, otherwise you might get into some unwanted hover effects and unexpected clicks on fading drops-down lists etc. You can even disable animations by setting the duration variable to 0 based on [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion?ref=angularspace.com). ## Making a directive Writing the actual directive would require 2 hacks. The first one is getting the renderer. You might think it’s as easy as injecting it, but unfortunately it does not always work. If your component is created via `ngComponentOutlet`, for example, or imperatively with `createComponent` the renderer is hardcoded in the [private API](https://github.com/angular/angular/blob/main/packages/core/src/render/api.ts?ref=angularspace.com#L246). That means if we want this to work all the time, instead of injecting the renderer we want to do this: ```ts inject(ViewContainerRef)._hostLView[11] ``` Don’t worry, [this](https://github.com/angular/angular/blob/main/packages/core/src/render3/interfaces/view.ts?ref=angularspace.com#L56) hasn’t changed since the introduction of Ivy engine, and we are only doing this until official support from Angular ships. We will also need the host element and the `ApplicationRef` so we can trigger a tick once the animation finishes. Our directive will apply `app-enter` class using the `host` property of its decorator, and we will add a listener for `animationend` / `animationcancel` events to remove the class. We will also use `afterNextRender` to check to remove the class if the host element `getAnimations()` list is empty (i.e. if there were no enter animations and, therefore, no event would fire). **Note:** we do not want to react to bubbling animation events from nested elements. In callback you can check that `$event.target === $event.currentTarget` or, if you value DX, you can add our [event plugins library](https://github.com/taiga-family/ng-event-plugins?ref=angularspace.com) and just write `(animationend.self)` which does the same thing under the hood. > Drop us a star if our package is helpful to you! The second hack is monkey-patching the renderer since we want it to keep elements in DOM and not remove them immediately. ## Patching the Renderer First of all, we need to track elements that have the directive applied to them, so that we can delay their removal. Angular Renderer has a prop called [data](https://angular.dev/api/core/Renderer2?ref=angularspace.com#data) designed specifically to store arbitrary data and that is what we’ll use. Our directive would add host element to the array and remove it from the array in `ngOnDestroy` (in a `setTimeout` so that it is removed after Renderer already processed it). Here’s what we will replace original `removeChild` method with: ```ts renderer.removeChild = (parent: Node, el: Node, host?: boolean) => { const remove = (): void => removeChild.call(renderer, parent, el, host); const elements: Element[] = data['app-leave']; const element = elements.find((leave) => el.contains(leave)); if (!element) { remove(); return; } element.classList.remove('app-enter'); const {length} = element.getAnimations(); element.classList.add('app-leave'); const animations = element.getAnimations(); const last = animations.at(-1); const finish = (): void => { if (!parent || parent.contains(el)) { remove(); app.tick(); } }; if (animations.length > length && last) { last.onfinish = finish; last.oncancel = finish; } else { remove(); } }; ``` First we try to find the element we are about to remove in the stored array of animated elements, and we will check how many animations it currently has (after removing `app-enter` class, in case enter animation is still playing). If there’s no such element we proceed with the original `removeChild` method. If we found that element we add `app-leave` class to it and query animations list again. What’s super cool is that if duration is set to 0, browsers will automatically synchronously report that there’s no new animations right on the next line after we added the class! So if the length of the animation list remained the same, we once again use original `removeChild`. If new animations appear in the list, we add listeners to the last one and call `removeChild` after it finishes. ## Trying it out That’s pretty much it. Now we can give it a shot and create a CSS animated slideshow using grid layout and simple animations. We will use fancy new [@starting-style](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style?ref=angularspace.com) rule and transitions to show off and to switch directions as we slide left or right. Grid layout will allow all images to be placed in the same spot without absolute positioning, meaning layout under the slideshow will be properly shifted down by the height of the images. We want our images to scale and fade so that’s what we will have as animations: ```css @keyframes fade { from { opacity: 0; } } @keyframes scale { from { transform: scale(0); } } ``` We will use class on parent to set direction of use sliding so that we can move images in different directions, based on `app-leave` class and `@starting-style` rule: ```css img { // … transition: left var(--app-duration, 500ms); &.app-leave, &.app-enter { animation-name: fade, scale; } @starting-style { left: -16rem; } ._forward & { @starting-style { left: 16rem; } } &.app-leave { left: 6rem; ._forward & { left: -6rem; } } } ``` Now all we need to do is show only 1 image at a time like that: ```html @for (image of images; track $index) { @if ($index === current()) { } } ``` And that’s it! You can check out a full live demo on [this StackBlitz](https://stackblitz.com/edit/angular-animations-css-demo?ref=angularspace.com). And if you are using the latest [Taiga UI](https://github.com/taiga-family/taiga-ui?ref=angularspace.com) version, this is [already available](https://github.com/taiga-family/taiga-ui/tree/main/projects/cdk/directives/animated?ref=angularspace.com) for you. In the next major release coming Summer 2025 we will drop @angular/animations dependency completely so that there’s no breaking changes. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/Screenshot-2024-11-13-at-15--1---1---3--1.jpg) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/angular-university-banner-4--3-.jpg)](https://angular-university.io/?ref=angularspace.com) ### AngularConnect Conf x Angular Space! URL: https://www.angularspace.com/angularconnect-conf-x-angular-space/ Last updated: 2025-07-08T09:52:03.000Z Thanks to fantastic Organizers of AngularConnect London Conf I have: > **2x 90% DISCOUNT CODE for AngularConnect Conference ( September 12-13, 2025 )** _This post is for subscribers only._ ### Spot the F-ups: Story Points, Days, and Planning URL: https://www.angularspace.com/spot-the-f-ups-story-points-days-and-planning/ Last updated: 2025-07-04T08:37:28.000Z **Planning and delivering projects is dead simple.** As long as you know what you are doing and what you are trying to achieve. Here’s the expanded recipe for avoiding the most common f-ups in business planning, management, and development + how to finally break free from the vicious circle of doom that traps so many teams. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/DALL-E-2024-10-17-22.44.04---A-lighthearted-and-slightly-humorous-image-representing-the-two-phases-of-project-management.-The-first-phase-is-depicted-with-characters-or-icons-iro.webp) ## Avoid the Classic Pitfalls as a Team ### **Iron Out the Details** - Start with the higher business - Validate their business vision and expectations from an engineering perspective. - Drill down through all the layers of business until you’ve covered every relevant stakeholder. - Involve designers early, but **validate designs with your leads first -** this saves you from setting unrealistic expectations that cannot be delivered in reality. ### **Build Trust-Based Relationships** Working with technical managers over the years taught me one thing: **Trust makes the machine run smoothly.** The best managers don’t demand hour-by-hour estimates. They trust the team to pick a workload they feel comfortable with, knowing that **delivery speaks louder than tracking spreadsheets.** This process isn’t rocket science it’s simple: - When delivery slows, they question the **process and blockers first**, not the devs. - When the process is sound but some issues persist, **only then evaluate individual performance when it's clear there is "a slacker on-board".** When you work out this kind of trust: - Morale rise, and teams work towards shared goals. - Stress drops because the team isn’t burdened by micromanagement. - Everyone feels accountable for their work without fear of arbitrary deadlines. --- ## **Building Trust with Management as a Developer** Building trust doesn’t happen overnight, but it’s not complicated. You need to show them you’re reliable, transparent, and care about the project as much as they do. Here’s how to do it: **Communicate Early and Often.** Don’t wait for problems to blow up. Tell management about risks, blockers, and delays as soon as you spot them. They’ll appreciate the honesty. **Back Up Your Decisions -** If you say, “We need two more weeks,” explain why. Show the trade-offs: “We can cut corners and meet the deadline, but it’ll hurt quality and require rework later.” Lay it out clearly so they see you’re thinking long-term. **Deliver What You Promise.** Nothing builds trust faster than delivering on your word. Start small - hit those early milestones, even if they’re minor. Consistency matters. **Don’t Sugarcoat Reality**. If something sucks, say it. Sugarcoating bad news to avoid conflict only leads to bigger problems later. Management needs to trust that when you say “It’s fine,” it really is. **Show Them You Care About the Big Picture** \- Management doesn’t just want to hear about your code. Show them you understand how your work impacts the product, the customer, and the business goals. This proves you’re not just a dev -you’re a partner in success. **Make Them Look Good**. Let’s be real - Managers love to report good news. When your team crushes a milestone, make sure they know. Let them take credit. Your reputation grows when they know you’ve got their back. --- ## Preventing the Circle of Doom as a Manager If you’ve been in IT long enough, you know the sad truth: **Unrealistic expectations kill projects.** It starts small but spirals out of control: 1. **Unrealistic expectations are set.** 2. Stress climbs, efficiency drops. 3. Sloppy code gets written. 4. Bugs pile up, rework increases. 5. Deadlines are missed, and management panics. 6. The pressure lands back on developers. 7. Repeat until everyone hates their life. This **circle of doom** never ends unless you: - **Stop overpromising to higher business.** - **Stop treating estimates as deadlines.** When you fix this, you break the loop, freeing your team to deliver quality software without soul-crushing stress. And if delivery slows, you’ll know exactly where the problem is. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/DALL-E-2024-10-17-22.42.55---A-lighthearted--slightly-humorous-image-representing-project-management--with-visual-elements-depicting-Story-Points-and-Days.-A-balancing-scale-with-.webp) ## Days vs. Story Points: Picking the Right Tool After ironing out your process, it’s time to plan your project. This is where you choose between **Story Points** and **Days**. The wrong choice here can set off the entire circle of doom - or save you from it. ### **Use Story Points When...** - You’re in an Agile environment that values **consistent delivery over tight deadlines.** - The business understands that **Story Points measure effort and complexity, not time.** - There is general understanding that team’s velocity is predictable after a few sprints, and no one is secretly converting points to hours. But keep in mind: - **Story Points fail when misunderstood.** If the business keeps asking, “How many days is 20 SP?” then you’re already doomed. Just do hours/days. ### **Use Days When...** - Deadlines are the main focus, and business wants hard time based estimates. - Stakeholders don’t care about Agile concepts and need straightforward timelines. But beware: - **Days can lead to overpromising.** Always include a buffer to account for complexity. --- ## The Golden Rule - Work Delivered is the Only Measurement After 10+ years of working with clients, one thing stands out for me: - **Work delivered is the only thing that matters.** Forget logged hours or Jira tickets. Focus on whether the team is delivering features that move the project forward. **If delivery slows:** 1. Question the **process and blockers first.** 2. Only after fixing those you can clearly verify if there is a problem with some underperforming teammates. This approach ensures you’re managing the team as a system, not as a group of interchangeable parts. Most delays are process problems—not people problems. **Note**: Most of the time tho, it's rarely a developer problem - in most cases things slow down and get chaotic due to bad business decisions and chaotic management. --- ## Lessons for Business and Management - Stop treating estimates as **promises set in stone.** When you frame estimates as flexible, you leave room for the team to adapt to complexity. - Learn from developers/leads - Trust the team to deliver without micromanagement. When devs feel trusted, they’re more productive, and morale stays high. - Make process improvement a habit - Fix the system before pointing fingers. Even when a developer struggles, it’s often a symptom of broader issues. --- ## Let's sum it up! Estimations suck in general if treated as deadlines. Teams can be very efficient without artificial estimations that don't make any sense. But if you absolutely need to chose 👇 1. **Story Points** work for Agile teams focused on **continuous delivery**, but only when the business understands the concept. 2. **Days** work for deadline-driven projects where time tracking is critical. Use them with a buffer to avoid stress and overpromising. 3. **Trust and process trump micromanagement every time.** ## Build a culture where work delivered is the only metric that matters! Hopefully, you’ll now spot these common mistakes a mile away - and have the all the tools needed to avoid them. Remember: trust your team, question your process, and break the circle of doom. ### **Profit stress-free. 💰** --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/Screenshot-2024-11-13-at-15--1---1---1---1-.jpg) ### Hidden parts of Angular: View Providers URL: https://www.angularspace.com/hidden-parts-of-angular-view-providers/ Last updated: 2025-07-03T04:08:47.000Z ## Introduction Angular is a powerful and extensive framework that offers an incredible range of capabilities. But with great power comes a steep learning curve. Mastering Angular often requires digesting a vast amount of material—from core concepts like dependency injection and routing to more advanced topics like reactive programming. While many of us become comfortable with the core mechanisms—dependency injection, change detection, routing, and so on—let’s be honest: even seasoned Angular developers don’t explore every corner of the framework. There are features that remain hidden, rarely discussed, or misunderstood. In this article, we’ll explore one of those hidden gems: `viewProviders`. They may not come up in everyday use, but understanding how they work can give you better control over your component architecture and service scoping. Let’s take a closer look at what makes them special—and where they might be useful in your own projects. ## What are viewProviders? In Angular, providers are like instructions that tell the framework how to create and deliver a given dependency when it’s needed. When you use providers, the service is available to the component itself, its template, any child components, and even to content projected into it using ``. On the other hand, `viewProviders` limit the service’s visibility strictly to the component’s view. That means it’s accessible only to the component and the elements declared directly in its template—but not to projected content or external child components. In short: `viewProviders` allow you to scope a service to a component's view, preventing it from leaking into projected or nested contexts. This difference is subtle but powerful. It allows you to better encapsulate logic and prevent services from being shared in ways you didn’t intend, especially when working with reusable or content-projection-heavy components. ![Difference between providers and viewProviders](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/providers-vs-view-providers.png) This diagram illustrates the difference in service scope between providers and viewProviders. With providers, the service is available to the component, its template, and any projected content (``). With `viewProviders`, the service is only accessible within the component’s own view — projected content is excluded. ## Use Case: Card with Dynamic Body Let’s put theory into practice. Imagine you’re building a `CardComponent` that displays a header, some internal logic, and a body whose content can be projected from the outside. The body could include forms, lists, buttons—whatever the consuming component wants. But you also want the `CardComponent` to manage some local state, like a `CardStateService`, which tracks interactions within the card (e.g., expand/collapse, internal toggles). Crucially, you don’t want the projected content to accidentally depend on or modify that internal state. Here’s what that might look like. ```typescript @Component({ selector: 'app-card', template: `

{{ title() }}

@if (!isCollapsed()) {
}
`, styleUrl: './card.scss', viewProviders: [CardState] }) export class Card { private readonly state = inject(CardState); title = input.required(); isCollapsed = this.state.isCollapsed; toggleCollapse = () => this.state.toggleCollapsed(); } ``` ```typescript @Injectable() export class CardState { private readonly _isCollapsed = signal(false); isCollapsed = this._isCollapsed.asReadonly(); toggleCollapsed(): void { this._isCollapsed.update((isCollapsed) => !isCollapsed); } } ``` Now, let's take a look at how the generic card can be used. ```html ``` ```typescript @Component({ selector: 'app-feature-card-content', templateUrl: './feature-card-content.html', styleUrl: './feature-card-content.scss' }) export class FeatureCardContent { // ❌ This will fail with ViewProviders — and that's a good thing private readonly state = inject(CardState); changeState(): void { this.state.toggleCollapsed(); } } ``` In this example: - `CardState` is scoped to `Card`’s view only. - `FeatureCardContent`, being projected via ``, does not have access to the state service — even though it's rendered inside the `Card`. This gives you a clean separation: internal state remains internal. So why does this matter? If we had used providers instead of `viewProviders`, `FeatureCardContent` would have been able to inject `CardState`. That could lead to tight coupling, unpredictable state access, or violations of encapsulation—especially in large, reusable UI libraries. By using `viewProviders`, we enforce a boundary, making our component more robust and predictable. ## Key Takeaways - `viewProviders` are a specialized Angular feature that limit the scope of a service to a component’s own view — not including projected content (``). - Use them when you need to encapsulate internal logic, especially in reusable or content-projection-heavy components. - They help prevent accidental dependency access from projected children and reduce the risk of service misuse or tight coupling. In short, `viewProviders` give you surgical control over where your dependencies live and who can access them. They’re a small feature with a big impact — especially when you care about isolation and clean component APIs. ## Outro Thanks for reading! If you’ve ever had services leak across projected content, or just want more control over DI boundaries, try `viewProviders` in your next component. Got questions or feedback? Let’s talk below 👇 --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/Screenshot-2024-11-13-at-15--1---1---3-.jpg) ### Strategy Pattern the Angular Way: DI and Runtime Flexibility URL: https://www.angularspace.com/strategy-pattern-the-angular-way-di-and-runtime-flexibility/ Last updated: 2025-06-30T12:04:07.000Z There are countless videos and blog posts explaining what the **Strategy pattern** is and how it can improve your Angular code. Yet almost every tutorial presents the pattern in *plain TypeScript* and quietly ignores the super‑power that Angular’s dependency‑injection (DI) system gives us. Take the canonical TypeScript sample from Refactoring Guru: [https://refactoring.guru/design-patterns/strategy/typescript/example](https://refactoring.guru/design-patterns/strategy/typescript/example?ref=angularspace.com). You’ll find dozens of copy‑and‑paste versions of that snippet on the web—and they work fine for what they are. Let’s take the next step and see how we can turn the pattern into a **production‑ready solution that embraces the framework instead of re‑implementing vanilla OOP inside it.** **In one sentence:** Strategy is a family of interchangeable algorithms—classes that share the same contract—between which we can switch at run time. Consider a familiar problem: every HTTP request can fail in dozens of different ways—409 Conflict, 500 Internal Error, or a custom business code like 400200\. A common first approach is to funnel all errors into one gigantic service and use a long switch to decide what toast, redirect, or retry logic to run. Very quickly that blob turns into a god‑object with tight coupling and sprawling dependencies. Adding or changing a single error path means editing existing code and praying nothing else breaks. The Strategy pattern lets us break that monster apart! Each error variant lives in its own class, we register them once, and the dispatcher chooses the right strategy at run time—with zero changes to the calling code. --- ## Tackling the error-handling problem with Strategy First, let’s introduce **the error domain**—these values are the *keys* we’ll use at run time to pick the right algorithm: ```ts export const ERROR_CODE = { NotFoundError: '400200', ServerError: '500200', Conflict: '409', Default: '0', } as const; export type ErrorCode = typeof ERROR_CODE[keyof typeof ERROR_CODE]; ``` ## The contract every strategy must follow ```ts export interface ExtendedServerErrorResponse { message: string; errorCode: ErrorCode; } export interface ErrorHandlerInterface { handle(err: HttpErrorResponse | ExtendedServerErrorResponse): void; } ``` ## An abstract helper to remove boilerplate `MatSnackBar` (or any shared dependency) lives only here, so every concrete strategy gets it “for free”. ```ts @Injectable() export abstract class BaseErrorHandlerModel implements ErrorHandlerInterface { protected readonly snackBar = inject(MatSnackBar); abstract handle( err: HttpErrorResponse | ExtendedServerErrorResponse ): void; } ``` ## Two concrete strategies ```ts @Injectable({ providedIn: 'root' }) export class ConflictErrorHandlerService extends BaseErrorHandlerModel { override handle(err: ExtendedServerErrorResponse): void { this.snackBar.open(`CONFLICT • ${err.message}`, 'close', { duration: 3000, }); } } @Injectable({ providedIn: 'root' }) export class DefaultErrorHandlerService extends BaseErrorHandlerModel { override handle(err: HttpErrorResponse): void { this.snackBar.open(`DEFAULT • ${err.message}`, 'close', { duration: 3000, }); } } ``` Each handler implements the same interface yet is free to inject extra services, perform side effects, or completely change the body of handle(). The result is a clean, testable class per error type instead of one “god service”. --- ## Variant A — Service Locator + errorInterceptor А few words about the service locator *Service Locator* is a design pattern that provides a **central registry** (the “locator”) from which the rest of the application *pulls* dependencies at run time. Instead of receiving collaborators via constructor injection, a client calls `locator.get(MyServiceToken)` and gets back the concrete instance it needs. The simplest way to plug our strategies into Angular is to keep a **hard-wired map** and combine it with the framework’s low-level `Injector` to implement a simple `Service Locator` setup. 1. `errorInterceptor` intercepts every failed HTTP response. 2. It looks up the error code in a global `ERROR_HANDLER_MAP`. 3. Using `injector.get(token)` it **pulls** the concrete strategy from DI. 4. The strategy’s `handle()` is executed. And here is the code sample. ### 1 - The global map ```ts import { ProviderToken } from '@angular/core'; import { ERROR_CODE, ErrorCode } from '../models/error-codes'; import { BaseErrorHandlerModel } from '../models/base-error-handler.model'; import { ConflictErrorHandlerService } from './conflict-error-handler.service'; import { DefaultErrorHandlerService } from './default-error-handler.service'; type ErrorHandlerMap = Partial>>; export const ERROR_HANDLER_MAP: ErrorHandlerMap= { [ERROR_CODE.Conflict]: ConflictErrorHandlerService, [ERROR_CODE.Default] : DefaultErrorHandlerService, } as const; ``` ### 2 - The Service Locator itself ```ts import { Injectable, Injector } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { ExtendedServerErrorResponse } from '../models/http-response'; import { ErrorCode, ERROR_CODE } from '../models/error-codes'; import { BaseErrorHandlerModel } from '../models/base-error-handler.model'; import { ERROR_HANDLER_MAP } from './error-handler-map.constant'; @Injectable({ providedIn: 'root' }) export class ErrorHandlerLocator { constructor(private injector: Injector) {} handle( code: ErrorCode, err: HttpErrorResponse | ExtendedServerErrorResponse ): void { const token = ERROR_HANDLER_MAP[code] ?? ERROR_HANDLER_MAP[ERROR_CODE.Default]; this.injector.get(token).handle(err); } } ``` ### 3 - The functional interceptor ```ts import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'; import { inject } from '@angular/core'; import { catchError, throwError } from 'rxjs'; import { ErrorHandlerLocator } from '../handlers/error-handler.locator'; import { ExtendedServerErrorResponse } from '../models/http-response'; import { ErrorCode } from '../models/error-codes'; export const errorInterceptor: HttpInterceptorFn = (req, next) => { const locator = inject(ErrorHandlerLocator); return next(req).pipe( catchError((err: HttpErrorResponse | ExtendedServerErrorResponse) => { const code = ( (err as ExtendedServerErrorResponse).errorCode ?? (err as HttpErrorResponse).status.toString() ) as ErrorCode; locator.handle(code, err); return throwError(() => err); }) ); }; ``` ### 4 - Registering the interceptor ```ts import { ApplicationConfig } from '@angular/core'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideAnimations } from '@angular/platform-browser/animations'; import { errorInterceptor } from './http/error-interceptor.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideAnimations(), provideHttpClient(withInterceptors([errorInterceptor])), ], }; ``` This approach is perfect when the list of error codes is small, stable, and you want one central place to audit who handles what. For more dynamic or plugin-style projects we’ll switch to a Dispatcher / self-registration model—but that’s Variant B. --- ## Variant B — Dispatcher + self-registration Where Service Locator pulls a handler **by key**, this flavour pushes the key **into the handler itself**. Angular’s multi-provider feature collects every implementation for us, and a tiny **dispatcher** decides which one to run at runtime. 1. Each concrete strategy exposes a `codes` array with the error codes it owns. 2. All strategies are registered under the same DI token with `multi: true`. 3. On bootstrap Angular injects an **array** of strategies into the dispatcher. 4. The dispatcher builds an in-memory map (`Map`). 5. The interceptor injects the dispatcher and simply calls `dispatch(code, err)`. ### 1 – DI token ```ts import { InjectionToken } from '@angular/core'; import { ErrorHandlerStrategy } from '../models/error-handler.interface'; export const ERROR_HANDLER_TOKEN = new InjectionToken('ERROR_HANDLER_TOKEN'); ``` ### 2 – Strategy classes (now with codes!) ```ts @Injectable({ providedIn: 'root' }) export class ConflictErrorHandlerService extends BaseErrorHandlerModel implements ErrorHandlerStrategy { readonly codes = [ERROR_CODE.Conflict]; override handle(err: ExtendedServerErrorResponse): void { this.snackBar.open(`CONFLICT • ${err.message}`, 'close', { duration: 3000 }); } } @Injectable({ providedIn: 'root' }) export class DefaultErrorHandlerService extends BaseErrorHandlerModel implements ErrorHandlerStrategy { readonly codes = [ERROR_CODE.Default]; override handle(err: HttpErrorResponse): void { this.snackBar.open(`DEFAULT • ${err.message}`, 'close', { duration: 3000 }); } } ``` ### 3 – The dispatcher ```ts import { Inject, Injectable } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { ERROR_HANDLER_TOKEN } from '../tokens/error-handler-token'; import { ErrorHandlerStrategy } from '../models/error-handler.interface'; import { ErrorCode, ERROR_CODE } from '../models/error-codes'; import { ExtendedServerErrorResponse } from '../models/http-response'; @Injectable({ providedIn: 'root' }) export class ErrorHandlerDispatcher { private readonly map = new Map(); constructor( @Inject(ERROR_HANDLER_TOKEN) strategies: ErrorHandlerStrategy[] ) { for (const s of strategies) for (const c of s.codes) this.map.set(c, s); } dispatch( code: ErrorCode, err: HttpErrorResponse | ExtendedServerErrorResponse ): void { const strategy = this.map.get(code) ?? this.map.get(ERROR_CODE.Default); strategy?.handle(err); } } ``` ### 4 – Functional interceptor ```ts import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'; import { inject } from '@angular/core'; import { catchError, throwError } from 'rxjs'; import { ErrorHandlerDispatcher } from '../handlers/error-handler.dispatcher'; import { ExtendedServerErrorResponse } from '../models/http-response'; import { ErrorCode } from '../models/error-codes'; export const errorInterceptor: HttpInterceptorFn = (req, next) => { const dispatcher = inject(ErrorHandlerDispatcher); return next(req).pipe( catchError((err: HttpErrorResponse | ExtendedServerErrorResponse) => { const code = ( (err as ExtendedServerErrorResponse).errorCode ?? (err as HttpErrorResponse).status.toString() ) as ErrorCode; dispatcher.dispatch(code, err); return throwError(() => err); }) ); }; ``` ### 5 – Registration ```ts import { ApplicationConfig } from '@angular/core'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideAnimations } from '@angular/platform-browser/animations'; import { ERROR_HANDLER_TOKEN } from './tokens/error-handler-token'; import { ConflictErrorHandlerService } from './handlers/conflict-error-handler.service'; import { DefaultErrorHandlerService } from './handlers/default-error-handler.service'; import { errorInterceptor } from './http/error-interceptor.interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideAnimations(), provideHttpClient(withInterceptors([errorInterceptor])), // self-registering strategies { provide: ERROR_HANDLER_TOKEN, useExisting: ConflictErrorHandlerService, multi: true }, { provide: ERROR_HANDLER_TOKEN, useExisting: DefaultErrorHandlerService, multi: true }, ], }; ``` Use it for larger, evolving projects or plugin-style architectures—anywhere the set of strategies changes often and you don’t want to edit a global map each time. Variant B is the better fit when you want a **modular, future-proof design**. Strategies register themselves, so you can add, remove, or ship them in lazy-loaded feature modules without touching central code. The result is a highly flexible system that evolves with your product instead of fighting it. Let's try to make some changes and see how our system works and how ready it is for changes. Now we have one handler for the error code. But what if we want to execute several handlers for one error code. Let's say we make Lego, when we can have several handlers for one code or event. It's very easy to do. Just switch to an array-based map—the rest of the code stays untouched. ```ts @Injectable({ providedIn: 'root' }) export class ErrorHandlerDispatcher { private readonly map = new Map(); constructor( @Inject(ERROR_HANDLER_TOKEN) strategies: ErrorHandlerStrategy[] ) { for (const s of strategies) { for (const c of s.codes) { const arr = this.map.get(c) ?? []; arr.push(s); this.map.set(c, arr); } } } dispatch( code: ErrorCode, err: HttpErrorResponse | ExtendedServerErrorResponse ): void { const list = this.map.get(code) ?? this.map.get(ERROR_CODE.Default); list?.forEach(h => h.handle(err)); } } ``` With this approach, we can specify the same error code for different handlers. This can be very useful for logging, for separating responsibilities, and for writing the most coherent code possible. --- ## Final takeaway - **Variant A (Service Locator)** — tiny, explicit, great for a short, immutable list of error codes that security can audit in one file. - **Variant B (Dispatcher + self-registration)** — virtually zero maintenance as your app grows; new strategies appear automatically, can live in separate libraries, and can even stack together for the same code. Where can Strategy + Angular DI shine? - **Transport layers** HTTP errors, WebSocket event types, server-sent events, GraphQL subscriptions. - **Front-end messaging** native DOM events, `postMessage` cross-window communication. - **Integration points** payment gateways, file-export formats, rich-text renderers, feature-flag treatments, analytics sinks. - **UI behaviour** per-role component variants, theme renderers, data-grid cell editors. The pattern is a hammer only when you have many nails. If your use-case is a single `if / else`, keep the code simple. But once the list of variants starts creeping up—or might tomorrow—drop in Variant B and let the architecture scale itself. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/06/Screenshot-2024-11-13-at-15--1---1---2-.jpg) ### Understanding Angular Deferrable Views URL: https://www.angularspace.com/understanding-angular-deferrable-views/ Last updated: 2025-06-27T10:29:58.000Z Angular 17 introduced a powerful feature called Deferrable Views (stable in Angular 18), revolutionising how we handle lazy loading in our applications. While traditionally we relied on dynamic imports with async/await and ViewContainerRef, Deferrable Views bring lazy loading directly to our templates with a much more elegant and flexible approach. ## Evolution of Lazy Loading in Angular ### Traditional Approach: Dynamic Imports Before Deferrable Views, implementing lazy loading at the component level required manual configuration through dynamic imports or router-level dynamic loading. Here's how it typically looked: ```typescript @Component({ template: "", }) export class ParentComponent implements AfterViewInit { @ViewChild("container", { read: ViewContainerRef }) container!: ViewContainerRef; async ngAfterViewInit(): void { const { LazyComponent } = await import("./lazy.component"); this.container.createComponent(LazyComponent); } } ``` This approach, while functional, had several limitations: - Required manual handling of the `ViewContainerRef` - Lacked fine-grained control over loading conditions - Mixed concerns between component logic and lazy loading ## Understanding Deferrable Views ### Key Concepts Deferrable Views introduce two fundamental concepts: 1. **Trigger**: determines when to render the component in the template 2. **Prefetch**: controls when to load the bundle from the server These concepts are distinct and can be used independently: - You might want to prefetch a component early but delay its rendering - Or trigger rendering immediately once specific conditions are met ### Basic Implementation The simplest implementation of a Deferrable View looks like this: ```typescript @Component({ template: ` @defer { } ` }) ``` By default, this will: - Create a separate bundle for heavy-component - Load when the browser is `idle` (after all page resources have been loaded) - Render once loading is complete ### Important Restrictions When working with Deferrable Views, keep in mind: - Only works with standalone components! - Deferred components shouldn't export tokens/constants used elsewhere (breaks lazy-loading) - Must be used in the parent's template (not with `ViewChild`) - Components imported by the deferred component can be either standalone or NgModule-based ## Advanced Features ### Control Blocks Deferrable Views come with three powerful control blocks: #### 1\. @placeholder ```html @defer { } @placeholder { } ``` - Shows initial content while the main component loads - Included in the main bundle, but it might be another one (for example if you have nested deferred blocks) - Optional minimum display time: `@placeholder (minimum 2s)` - Content inside `@placeholder` block is eagerly loaded #### 2\. @loading ```html @defer { } @loading { } ``` - Displays while the bundle is loading - Included in the main bundle - Supports timing parameters: - `@loading (minimum 2s)` - `@loading (after 1s)` - Combined: `@loading (after 1s; minimum 2s)` - You probably won't see the loading block if you have `after` AND fast loading times #### 3\. @error ```html @defer { } @error { } ``` - It doesn't protect from runtime errors and also there is no way to retrigger the loading after a network failure. - Content inside `@error` block is eagerly loaded. ### How do @placeholder and @loading differ? While `@placeholder` and `@loading` might appear similar, they handle distinct moments in the deferred content lifecycle. `@placeholder` acts as a pre-loading visual, shown immediately, even before bundle loading begins. `@loading`, conversely, appears only during the active bundle download, disappearing once the content is ready. Essentially, `@placeholder` holds the spot, while `@loading` signals active retrieval. ### Trigger Types #### Predefined Triggers (used with on \*) 1. **idle** (default) ```html @defer (on idle) { } // equivalent to: @defer (on idle; prefetch on idle) { } ``` 1. **viewport** ```html // With placeholder @defer (on viewport) { } @placeholder { } // Without placeholder
Title
@defer (on viewport(title)) { } ``` 1. **interaction** ```html // With placeholder @defer (on interaction) { } @placeholder { } // Without placeholder
Title
@defer (on interaction(title)) { } ``` 1. **hover** ```html @defer (on hover) { } @placeholder { } ``` 1. **immediate** ```html @defer (on immediate) { } ``` 1. **timer** ```html @defer (on timer(5s)) { } ``` **Note**: Duplicate triggers of the same type are not allowed within a single deferrable view. #### Custom Triggers (used with when \*) Use `when` for custom conditions: ```typescript @Component({ template: ` @defer(when showDetails; prefetch when isNearBottom) { } `, }) export class MyComponent { showDetails = signal(false); isNearBottom = computed(() => this.scrollPosition() > 0.8); } ``` ## Performance Optimization Patterns ### Multiple Components in One Block ```html @defer (on viewport) { } ``` - Each component gets its own chunk - Loaded together but independently bundled ### Strategic Prefetching ```html // Load it when we are on the viewport and show it when we interact @defer (on interaction; prefetch on viewport) { } @placeholder { } // Load it when load variable is true and show it when show variable is true @defer(when show; prefetch when load) { } ``` ## ViewChild and Defer Block Compatibility Prior to Angular 18.2.1, developers encountered an issue where the deferrable view would not work properly in certain scenarios. This problem specifically occurred when referencing heavy (third-party) components outside of parent component imports. When components were referenced outside the parent component imports, it interfered with proper tree-shaking functionality, preventing the deferrable view from working as intended. The fix involves two key approaches: - Version Update Solution: - Upgrading to Angular 18.2.1 or later resolves this issue out of the box - This version includes specific fixes for deferrable view functionality - Type-Only Import Solution: - If you need to support older versions, you can implement a workaround using type-only imports: - Example from [Angular Love Autumn Camp](https://youtu.be/ZLkmvc4AC1A?t=1327&ref=angularspace.com) by [Dawid Kostka](https://www.linkedin.com/in/dawidkostka/?ref=angularspace.com) ```typescript import { Component, computed, viewChild } from "@angular/core"; import { ChartComponent, type ChartComponent as ChartComponentType, // solution Angular < 18.2.1 } from "./chart.component"; @Component({ selector: "app-parent", standalone: true, imports: [ChartComponent], template: ` @defer (on viewport; on idle) { } @placeholder (minimum 1s) { ... } `, }) export class ParentComponent { readonly chart = viewChild("child"); readonly chartId = computed(() => this.chart()?.id); } ``` ## @for inside vs outside deferrable view The recommendation is to place `@defer` outside `@for` when displaying uniform components, as it creates fewer views. However, use deferrable view inside `@for` when you need different heavy components rendered conditionally within the loop. ```html @for (item of items) { @defer { } } @defer { @for (item of items) { } } ``` The choice impacts performance through the number of embedded views and deferrable views Angular needs to manage at runtime. To learn more about techincal aspects you can check [Matthieu Riegler post](https://riegler.fr/blog/2023-10-08-defer-part2?ref=angularspace.com). ## SSR Considerations When using Deferrable Views with Server-Side Rendering: - Browser events aren't available during SSR - Only `@placeholder` blocks render on the server - Triggers activate post-hydration - Plan your initial loading state carefully ## Incremental hydration Incremental Hydration extended the behavior of @defer (experimental in v19). [Jessica Janiuk](https://github.com/thePunderWoman?ref=angularspace.com) started an [RFC](https://github.com/angular/angular/discussions/57664?ref=angularspace.com) that leverages `@defer's` capabilities to control hydration timing. Under the hood: - Deferrable views define hydration boundaries - Server renders static HTML first (both eager and defered components) - Client hydrates components based on triggers: ```html @defer (hydrate on viewport) { } // On viewport @defer (hydrate when condition) { } // When condition met (static only) @defer (hydrate never) { } ``` ## Best Practices ### Performance Optimization - Place deferrable views outside loops when possible - Use prefetch strategically for better UX - Keep placeholder content light ### User Experience - Always provide meaningful placeholder content - Consider loading states carefully - Handle errors gracefully ### Code Organization - Group-related deferred components - Keep trigger logic clean and maintainable - Document loading strategies ## Conclusion Deferrable Views represent a significant step forward in Angular's lazy loading capabilities. They provide: - More granular control over component loading - Better user experience through built-in loading states - Cleaner, more declarative code Now stable as of Angular 18, this feature provides a robust foundation for building more performant and user-friendly Angular applications. Remember that Deferrable Views load parts of a component template, while the lazy-loading mechanism loads a whole component. Also, Deferrable Views are not linked to the Router. ## Thanks for reading so far 🙏 Spread the Angular love! 💜 If you really liked it, share it among your community, tech bros and whoever you want! 🚀👥 Thanks for being part of this Angular journey! 👋😁 --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/06/Screenshot-2024-11-13-at-15--1---1---1-.jpg) ### The Need Of Failing Before Succeeding URL: https://www.angularspace.com/the-need-of-failing-before-succeeding/ Last updated: 2025-06-13T09:32:44.000Z I used to think the whole try-and-fail wording was just a catchy phrase people threw around to sound deep. Turns out it has some truth to it and the fail part is very real. Failing sucks, and many times, It's not just some feel good “learning opportunity”. It usually comes with blame, criticism, and mainly taking responsibility for your actions. I feel like in tech we don't share our experiences so often, even if some may come with valuable lessons, therefore I wanted to write a smaller article where I'll share some of my own screw-up decisions I was sure were right (they weren't), the lessons they taught me, and how I became a senior developer by age 27\. I want to underline that these are my personal experiences, not universal rules. Every project has its own variables, such as people, tech stack, timeline, and budget. What failed for me might work perfectly for someone else under different circumstances. Hope these short stories will show you that we all suck, and being a senior dev just means you've made enough bad calls to know which ones to avoid next time. ## Quick Refactoring *“hmm, look at that, old code - I should refactor this, doesn't look that complicated”.* Classic mistake only juniors make, right? Well, I've made this mistake at least once a year for the past seven years. Every. Single. Time. And somehow, it was always more complicated than I initially thought. First, you have to convince your manager to allocate time. Believe me, he wants nothing else to hear just why exactly we need to change an already existing production code, used by thousands of paying customers, just because you don't like the syntax. Good luck selling that. Even if you manage to sell the idea, then what? You've got a deadline. Are you 100% sure you understand every edge case in that old legacy code? No? Are there tests? Probably not. So now you're hoping, that your shiny new version doesn't explode. And when it does, you just became the responsible person for it. Worst case scenario? You're deep in your “harmless little refactor,” missing your deadlines, stress levels through the roof, burning unpaid overtime to fix a mess you created entirely voluntarily. Refactoring isn't bad. In fact, it's often necessary for long-term maintainability. Simple refactoring might include upgrading dependencies, where newer library versions can fix some security issues or reduce the overall bundle size through optimizations like tree-shaking. Other small refactors with high impact can include reducing repeated DOM access inside loops, extracting duplicated logic into reusable functions, or replacing nested callbacks with async/await for readability. However, be aware of the risks you're walking into. If you don't fully understand the purpose and scope of the code, don't refactor it blindly. Always have a clear reason for the change, ideally with your manager's blessing. It's critical to understand why the code exists in its current form and what edge cases it handles. ## Pushing Side Projects Everyone tells you side projects are essential. They help you learn new tech, experiment freely, and build confidence without the pressure of deadlines. The problem starts when you delude yourself into thinking your side project is definitely the next unicorn SaaS startup. My most “educational failure” was an app called GGFinance. It started as my Master's thesis and later turned into a long nights work. The concept was decent - a stock portfolio simulator aimed at schools. Students were creating a fake portfolio by trading stocks and the app was calculating many financial metrics, displayed in charts (people like charts), and students competed with each other. I even made [a YouTube video](https://www.youtube.com/watch?v=77E7O%5FZu4tA&ref=angularspace.com) about it. The app is long gone, of course. I had examples, eToro, XTB, various data-vizualizations apps, so clearly, my idea had needed to work. Or so I thought. I learned that having users does not automatically mean you'll make money. Turns out, building a profitable product requires an actual business plan. Who would have thought. I thought if I built it, customers would come, and pay. No. You need marketing, user research, pricing strategies, customer support... basically everything besides development. I invested 1,500+ hours in that app. Worth it? For the tech lessons, yes. I was optimizing the app, solving many edge-cases, and from the tech side, it was the most complex and best learning experience. For the revenue? I only saw red numbers. Most projects fail, and it still surprises us, that our idea does too. And just because you put more work into something it still does not mean it will succeed. Sometimes we have to admit our loss and move on. Still, I'll always advocate for side projects. Not because they'll make you rich, but because they'll make you better. Company projects tend to revolve around the same stack, the same patterns, and often avoid "risky" experiments, for a good reason. It's easy to stagnate. Side projects give you space to try a new framework, build something from scratch, and break stuff without consequences. You'll learn faster because you're the one making the decisions. And sure, it might be exhausting after a long workday, but even dedicating a couple of hours a week keeps you sharp. ## Over-Evaluating Your Skills I started my career as an IT helpdesk guy just before my twenties. It was a small company, around 100 people, only 2 of us on the helpdesk. We were receiving every user request thought an email. It was hard to track time, status and who was working on what. And some email occasionally even got “lost” if the request was too hard on us. I was already learning some PHP, Jquery and MySQL, and then I came up with a brilliant idea to create an internal ticketing tool, a small Jira. Enter the Dunning-Kruger effect. I massively overestimated my abilities. I thought, how hard could it be? A few forms, some login logic, slap on a bit of CSS, done. Instead, I ended up learning how little I actually knew, especially about things like async flows, multi-role authentication, and database design. For my first attempt I designed the database with 5 tables, shipped the product after some uncomfortable long time, and felt like a genius. The app was \~working. I felt excited as the application was growing and slowly the whole company started to use it; however, new and new feature requests were coming in, and as the only coding guy, I had to solve them all. Over the next two years, the project went through multiple iterations, and I even wrote a custom script to migrate my 5-table database structure into a new 25-table one. Thankfully, at the time we only had 400 tickets, but man, database migrations are the worst. This project had its ups and downs. The fascinating part is that even five years after my last commit, it is still in use, making it my most successful solo project. But it had its cost with plenty of frustrations, lots of learning, and many weekends spent coding while studying at University. I learnt that before promising the stars, make sure you understand the destination, and at least have a rough idea of how to get there. ## Using The New Shiny Tech Once, I was assigned as the frontend dev on a greenfield project. We got to pick the stack. I had been playing with GraphQL during weekends and loved how flexible it was. You shape your own queries, it generates TypeScript types. What's not to like? So, I thought: “If it works in my toy projects, it'll be perfect for this big production app too”. Backend team was on board. Management was convinced. We were ready to change the world. Fast-forward 4–5 months. Everything works fine... on localhost. Then we move to real servers and suddenly, queries are slower than a government website. Now what? Turns out GraphQL's has its infamous N+1 problem, where it hammers the database with calls for every nested entity. We spent another 3-4 months optimizing queries and rethinking everything. Now, similarly as in the **Over-Evaluating Your Skills** section, what did I do wrong here? For starters, I was learning GraphQL for 3-4 weeks. I quickly scanned the documentation (who needs docs, am I wrong?) and I used this tech on a small project on localhost. We sold GraphQL to the company based on our excitement, not a deep understanding of how it worked, and we ignored some cautious voices from teammates. Keep in mind that once you push a new tech in a project, you may became the responsible person for it. Being the go-to guy is not bad, if you know what you do. The problem becomes when you spend 2 hours learning a new library and thinking you mastered it. I would like to emphasise that I don't think GraphQL is a bad technology. The company I work for now uses GraphQL in one project, and all I hear is positive feedback. It's a bit like using Angular without OnPush and then blaming Angular for being slow — the tool isn't the problem, the implementation is. This is more of a story of me, a young, naive developer seeing a new tech through rose-colored glasses, only reading the praise, not understanding the full picture, and convincing others to take it to production. ## Not Asking For Help Back in my early days, I wanted to be a Java developer because, well, that's what they taught at University. Then one day at work, I was given a tiny frontend task. Create a dashboard with two pages. One to display external links as buttons and one to display admin logs in a table. For an experienced person this would be 1-2 days of work. I was working on it for 4 weeks. The reason? Since I was new in web, I had no idea what CORS, Proxy, JWT token, Cookies, Sessions, Application State event meant… and it was a first time I heard that javascript have libraries - Angular/React? Instead of asking for a help of my colleagues, I started googling and went down to a rabbit hole of information. There is a fine line between “I don't want to look stupid so I will google it” and “I have no idea what I am doing”. Junior developers often let ego get in the way. We'd rather spend 40 hours creating a terrible solution ourselves than ask for help and risk looking clueless for 5 minutes. In the end with tears in my eyes I went to my superior and asked him for helped. He scolded me like a 12 year old child for waiting such a long time, but we solved all problems. Now, as a senior, I've learned that saying “I don't know” is not a weakness. It's efficiency. It means I know when to ask the right person for help and move on. It's worth mentioning that not asking for help has a dark opposite - asking for help too much. When you're new to a team, it's totally okay, even expected, to ask questions or get clarification when you're stuck. But be mindful of how you ask. You don't want to turn your colleague into your personal Google and just wait for them to hand you answers. Instead, always share what you've already tried or the approaches you're considering. That way, your teammate can give more targeted advice and it shows that you've put in some effort before reaching out. ## Poor Project Management I was so happy when I first landed my first junior programming job. I wasn't questioning anything and I thought what they were doing was the gold standard in the tech industry. Well... not exactly. I had basic Git skills — commit, push, pull, more or less what I do now. But what surprised me was that our team of five devs, were all pushed directly to the main branch. No feature branches, pull requests or code reviews. Straight up YOLO to main. As the project was heading toward a big launch, every week our managers would demo new features to the client. Weekly presentations, that created an extreme pressure on us to deliver features fast. I remember that the day before one of the presentations, I wanted to finish my task. I was doing some changes on the checkout page and commented out the payment button functionality for testing. Since no PR reviews were done, I forgot to uncomment the code, I made a commit and pushed into main. Happy with my changes I closed the PC and went home. Next day demo time. The client notices the payment button doesn't work. “But it worked last week…” Hmm. They dig into the Git log. Whose name do they see? Mine. Awesome. I received some gentle feedback, which motivated me to check each file changes before committing them. I felt horrible at the time. But looking back, the real problem wasn't one junior dev pushing a broken commit, it was a complete lack of process. No reviews or QA or checks. If a bug makes it to production, it's never just one person's fault. It means something broke in the process the team is using. ## Letting Your Emotions The Better Of You We're all human (at least most of us), so no matter how many coaching sessions we attend or how many times we're told to “think before responding,” there will always be moments when emotions take over and we say something we probably shouldn't. Two classic examples are salary negotiations and PR reviews. Let's start with salary. You're doing your job and doing it well. Over time, you take on more responsibility, lead more initiatives, and help others, all while your paycheck stays the same. At first, you're okay with it. But months go by, and the frustration quietly builds. Especially in tech, where your teammate, doing the same work (or occasionally even less), might be getting paid more. One day, you snap. You storm into your manager's office (or call him on MS Teams), list everything that's wrong, demand a raise and maybe even threaten to quit. Now… was that a great strategy? Sure, you might get what you asked for, but it'll likely damage your relationship with your manager. And even if you “win,” you've created tension that can hurt you down the line. A better approach? Come prepared. Gather what you've accomplished over the past few months. Show the extra responsibilities you've taken on. Then, calmly ask if it would be fair to revisit your compensation. And remember, even if this has been bugging you for months, it's probably the first time your manager's hearing about the request. They won't have a solution immediately. You're not trying to corner them. You're trying to solve the problem together. I see this a lot, especially with junior developers. At first, they feel “lucky to have a job” and just patiently wait for their manager to bring up a raise... and nothing happens. But here's the thing, you chose this company, and they hired you because they see value in you. If they want to keep you long-term, this conversation needs to happen. You should be having regular performance reviews where you lay out your contributions. That's the perfect time to start open up this topic. It may feel awkward at first, but these small, honest talks are what prevent bigger problems later on. Now, on to PR reviews. Someone once told me code reviews are like standing naked in front of a mirror, all your flaws are right there, impossible to miss. When you submit a pull request, you usually feel pretty good about it. You expect a comment or two, but nothing major. I remember once I submitted a 200-line PR and got 50 comments. Whether I was frustrated is not even a question. I almost wrote something regrettable to that person. At least it was still in morning and I had my willpower to stop myself. I took a break, cursed a bit, and then calmly called that person to walk though the feedback. The suggestions were more about the way of writing the code, rather than the business logic. The call was long and painful, but we came to a common ground, that appealed both of us and merged the PR. Through the time I cultivated a positive mindset in a sense that nobody really wants bad for you. People just want to do their job well and sometimes they aren't the best at communicating. The message that you interpreted personally may have just been a rushed response from them. If you get angry, you need to sit with your emotions, let them pass a bit and just then start replaying. It is always you with your teammate against the problem and never you against him. ## NgRx for State Management I remember around 2018 there was a big push towards NgRx State Management. As your application scales, centralizing state management with a library like NgRx can help avoid code mess and make things more predictable. The argument was that relying on a proven library reduces the risk of introducing bugs through ad hoc state logic. So I started to learn NgRx and this was the famous diagram I was staring at for a week until I thought understood what It meant. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/06/ngrx.jpg) NgRx Overview I'm not going to explain the concept here, but let's just say it's not exactly beginner-friendly. I was trying to shift from writing imperative code to thinking declaratively, and that's a mental jump. At the time, we were refactoring our custom RxJS Observables state into this NgRx concept. It was largely driven by one external senior developer, so we didn't question his decisions that much. I assumed that I lack some knowledge to understand this higher power wisdom. But once we finished the migration, things got weird. Suddenly, our once relatively clean project had exploded in size. We had three to four times more files, and they were scattered across different folders — actions, reducers, effects, selectors, facades, and sometimes even duplicated types. What used to be a simple state update now required touching five different files, across multiple layers of abstraction. Even worse, onboarding new developers got tricky. Instead of showing them the logic in one place, we had to walk them through a mental map of how state flowed through the app. Looking back, the mistake wasn't using NgRx. The mistake was using a powerful tool without fully understanding the paradigm shift it required and underestimating the cost of added complexity for a relatively small team. NgRx isn't "bad", it shines when it is used correctly. You can have the best hammer in the world if you are unable to use it properly. Since some years have passed, I've seen NgRx introducing `Component Store` or even `NgRx Signals` which is a much lightweight implementation of the above picture. After some break I picked it up again and with a better understanding, and I wrote an article on [Angular State Management - Imperative, Declarative, Ngrx and SignalSlice](https://dev.to/krivanek06/angular-state-management-how-to-keep-your-sanity-1oin?ref=angularspace.com) From myself, I worked on projects with less than 500K lines of code on the frontend, where I prefer using custom signal based solution to manage application state. That said, I now understand why libraries like NgRx exist, and when they really shine. In large-scale applications with many developers, strict separation of concerns, predictable flows, and centralized debugging become critical. NgRx's declarative pattern can help enforce consistency across teams, make testing easier, and prevent accidental side effects. It's always best to try it out yourself and draw your own conclusions. ## Key Takeaways Honestly, and sadly, I could go on about all the mistakes I've made. But here's the real takeaway: we all screw up. Sometimes a little. Sometimes catastrophically. And that's exactly how you grow. What makes a senior developer isn't knowing all the answers. It's knowing which bad decisions to avoid because you've already lived through the fallout. Experience doesn't just teach you what to do, it teaches you what not to do. So fail. Then fail better. And maybe one day, you'll be writing blog posts like this, after you've finished crying over your failed side project. Hope you liked these short stories. Feel free to share your thoughts, catch more of my articles on [dev.to](https://dev.to/krivanek06?ref=angularspace.com), connect with me on [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com) or check my [Personal Website](https://eduardkrivanek.com/?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/06/Screenshot-2024-11-13-at-15.52.00--1---6---1---3---1-.jpg) ### How to Grow from Senior to a Lead Role URL: https://www.angularspace.com/how-to-grow-from-senior-to-a-lead-role/ Last updated: 2025-06-09T07:55:34.000Z ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/sr-to-lead.jpg) ## Intro As engineers, we are used to having things in our control - the outcome of the task normally depends on our implementation. Growing from senior to a lead role requires a mindset shift. You become a multiplier for the team and helping others around you becomes one of the main priorities. The overall success of the team and projects is what becomes important. Let’s take a look at a situation that can happen after becoming a senior and staying in the role for some time. ## Without action, you can feel stuck in the senior position You have grown to a senior position and you are feeling ecstatic. The first thing after you become a senior, you want to ensure that you have all the necessary skills covered. You learn and grow. After some time passing, you are still in the senior position and now it feels like you are not growing anymore. It’s quite common for engineers to feel stuck in the position and not know which steps to take to grow toward a tech lead / team lead position. I've been there and I hesitated for too long towards the next step. I believe that my growth was stagnating because I didn’t know that. I was overthinking about my career path. It’s really important to continue to grow and find ways to do that. I like to say, that if we are not growing, we are stagnating. There are so many ways that we can grow in software development. And a lot of it is not technology-related at all. Before we get into the ways to grow, let’s first make a distinction between the Tech Lead and Team Lead role. ## Difference between Tech Lead and Team Lead The complete responsibilities of both vary from company to company, but one thing is clear. Team Lead role is more focused on people management and Tech Lead role is more focused on leading the technical vision and implementation. Mostly, the companies are using one or both of the roles. In my teams, I like to have both. Which role you should go for? It’s totally up to you, both roles are great starting points to grow in any of the paths - management (engineering manager), IC (Staff Engineer) or architecture (Software Architect). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/sr-to-lead-image.jpg) Since the Team Lead is more people-focused it will give you a bit of an edge for the management path and Tech Lead role will give you a bit of an edge in the IC and architecture path. But you can utilize the skills that you acquired in any of the paths. Now, let’s get into my top tips that can help you grow into a lead role. My perspective and recommended steps are backed by my experience as an engineer who has grown to lead roles and as a manager who has promoted engineers to lead roles. ## Make sure to let your manager know about your goals and aspirations This is really important because if your manager doesn’t know about your goals and where you wish to grow, they won’t be able to put you in the position to showcase the skills needed to grow to that position. In a 1:1 meeting, share your excitement about the role and ask about direction and how can you get there. Or even better, by reading this article you would already know what skills are needed and how you can showcase them, start doing them ASAP and let your manager know what you are doing. There is also a limited scope that your manager can see about what you are doing daily, so it’s really important to keep all of your wins close and note them down. Let’s get into that next. ## Keep a brag list of all the wins that you achieved I highly recommend keeping a brag list of all the wins that you have made. The reason is close to what I mentioned above. Your manager doesn’t see or probably note down all of the wins that you achieved. Therefore it’s really important that you do that and keep the notes close to you, so when the time comes to talk about the promotion, you have all of the reasons to promote you within your notes. You have successfully onboarded a new engineer, you are mentoring others or you have played a key role in delivering an important project. Make sure to note all of this down. It will help you immensely. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/brag-list-image.jpg) What I did and I still do (as a CTO) is: I keep all of the wins I achieve in my Notion page as bullet points. I use Notion for all of my notes, so in case I get asked about my achievements, I always have it with me. No matter the role you currently hold, this is really important to do. It will help you a lot to showcase your value and move forward in your career. ## Become the go-to person or an expert in a certain domain. Is there something that you have a really good understanding of? Is there a certain technology or library that you know better than anyone else? Make sure to take responsibility and ownership to help everyone else get better at it. Become a person that others will go to when they would need help. Write documentation, prepare learning sessions and do code reviews on that particular part. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/sr-to-lead-image3.jpg) This provides immense value to the organization because your efforts are considered as multipliers (you make everyone around you better), which is the essence of being a great leader. Just doing this alone will create opportunities for you, because you developed the reputation of a responsible person who can get things done and at the same time help others. ## Propose an impactful improvement to the codebase and own the implementation Propose an impactful improvement to the codebase and if it is accepted, you should also volunteer to lead the implementation. This would automatically make you a lead for that implementation. The more impactful it is, the bigger the value is going to be. Successfully leading the implementation will be one of the main factors for your promotion. At the same time, you will learn a LOT about being a leader, how to influence and how to successfully finish projects. You will also develop a reputation as a person who can get things done. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/propose-impactful-improvement.png) This is very powerful because one of the main important things managers look for in people is that they can take “not so clear requirements” and deliver the exactly needed solution without supervision. ## Become product-minded Figuring out what creates the biggest impact and contributes to the business value the most is one of the most important things you can do as an engineer. You are good at technical things and at the same time, you understand the business side and what it takes to delight the customers. You provide immense value with that understanding. "Why are we building this thing?" - This is one of the great questions to ask, before starting to implement a new functionality. It gives you insights and the motivation behind it. Being curious about the product and the business will enable you to create great technical solutions that customers are going to love. To help you with this, I have prepared a list of questions that you can use. ### 🎁 Notion Template: List of questions to ask before making a technical decision Use this list of 35+ product/business-oriented and technical questions to really understand WHY something is needed. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/notion-template-list-of-questions.webp) You can find the template here: [🎁 Notion Template: List of questions to ask before making a technical decision](https://calico-cabinet-fbf.notion.site/List-of-questions-to-ask-before-making-a-technical-decision-39efbd6174474bf0947a8976800eb73c?ref=angularspace.com) ## Start writing online if you haven’t already Writing online regularly had so many benefits for me that I just had to include it in the list. You help others and at the same time, you cement your knowledge on particular topics. It helps you as well with your personal branding and showcasing you as an expert in a particular field. If you are a Senior Software Engineer in order to get to the lead role, you need to showcase your leadership skills and that you can successfully lead the team. Writing regularly, where you share your knowledge on particular topics will help you a lot (plus points if you write about leadership topics as well). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/sr-to-lead-image4.jpg) [](https://newsletter.eng-leadership.com/p/100-discount-code-for-products?ref=angularspace.com)I like to say that good writing is a superpower for engineers. - Giving a lot of context in a few words. - Writing very easy-to-read and understand documentation. - Doing great code reviews. This automatically transfers to writing good code as well. The mindset is the same. ## What others are saying? I have asked the LinkedIn community to share their #1 tip for growing from senior to a lead role. And there were a lot of great responses. Here they are: > “Sitting back and not asking for feedback is the worst thing you can do as an engineer. If you want to improve, there has to be a constant feedback loop.” > — [**Ankur Tyagi**](https://www.linkedin.com/in/theankurtyagi/?ref=angularspace.com) > “Become the go-to person for your engineering product area.” > — [**Caleb Mellas**](https://www.linkedin.com/in/calebmellas/?ref=angularspace.com) > “Ask for feedback on what to do to get better. Ask for support. Be patient.” > — [**Daniel Lock**](https://www.linkedin.com/in/daniellock/?ref=angularspace.com) > “The brag list will also help jog your memory for writing resumes and answering behavior and technical deep dive interviews.” > — [**Mike Thornton**](https://www.linkedin.com/in/devdetails/?ref=angularspace.com) > “Find opportunities and ask for them instead of waiting to be given.” > — [**Anemari Fiser**](https://www.linkedin.com/in/anemari-fiser/?ref=angularspace.com) > “Take ownership and be a self-starter (i.e., can work with minimal supervision, brings new ideas to the team).” > — [**Markus Kaufmann**](https://www.linkedin.com/in/mkaufmann-ciso/?ref=angularspace.com) > “Understand what it means to lead people. It’s not the same as being a craftsman at your current role. It is about people and caring.” > — [**Tim Ward**](https://www.linkedin.com/in/tim-ward-0b88b11/?ref=angularspace.com) > “Develop further into becoming a safe pair of hands releasing to Prod, Looking after Prod and Designing systems.” > — [**James Parra**](https://www.linkedin.com/in/jamesparra/?ref=angularspace.com) ## Last words There are a number of different ways to grow from senior to a lead role. You can either use some of them or all of them, it’s up to you. Keep in mind that the more you showcase your abilities, the faster you are going to be recognized as a candidate that would be great for the role! ## Learn more To learn more, you can check out the [Engineering Leadership newsletter](https://newsletter.eng-leadership.com/?ref=angularspace.com), where I share similar tips and advice on how to be a great engineering leader. I publish 2 new articles every week! --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/Screenshot-2024-11-13-at-15.52.00--1--2.jpg) ### Angular Error Handling URL: https://www.angularspace.com/angular-error-handling/ Last updated: 2025-06-09T07:55:44.000Z ## Introduction Error handling is as much of an important topic as it is also hated, and even more so, overlooked. Of course, we all enjoy authoring cool features, fascinating animations and beautiful UIs, but not so much do we love writing a bunch of code whose only purpose is to save us when something goes wrong. However, an important part of a developer's journey to maturity is realizing that errors are inescapable. A third-party library might contain a bug; a network request might fail; something might be wrong with the end user's machine. In all such scenarios - and more - we need to be able to meet these errors gracefully, and not allow our application to break because of simple scenarios that we are capable of anticipating. So, let's get started with first covering the basics! ## Synchronous Error Handling Synchronous errors are rare in Angular apps, since around 90% of accidents arise when making network calls and such; however, they might happen, and here are a couple of examples: 1. Using a third-party library which raises an error in some circumstances (wrong format of data passed, etc) 2. Using a third-party library which has a bug that causes an error to be thrown 3. Using your own project's code that throws errors in some cases 4. Using your own project's code that has a bug Now, handling all of those is pretty simple: 1. Examine the circumstances under which the error is thrown, and fix your usage of the library 2. There isn't much we can do to the library code that has a bug, but we can report it to the library author, and, in our code, add a `try`/`catch` statement to handle the error gracefully (or, if you have the time and motivation, please consider actually fixing the bug in the library and sending a PR) 3. Use the code in our codebase correctly so as not to get an error; or, if the error itself is raised wrongfully, change the underlying code 4. Fix the bug if possible or find a workaround not to get the error, or actually find the root cause of the issue to avoid introducing another bug or increasing complexity: adding `try`/`catch` to this code should be a last resort, as it can make the code appear logical, all the while it actually covers up for a bug. Consider adding a comment explaining the situation if you end up using `try`/`catch` after all Now, we have discussed "local" errors and handling them; errors that happen in very particular situations and are handled in very particular ways. But what if we need a way to intercept errors and do something on a global scale? Let's talk about it. ## Global Error Handling Angular provides a way to handle errors globally, and it is done by implementing the `ErrorHandler` class. This class has a single method, `handleError`, which is called whenever an error occurs in the application. To use it, we need to create a new class that implements the `ErrorHandler`, override the `handleError` method, and then re-provide the `ErrorHandler` in our application config to make it use our own implementation: ```typescript @Injectable() export class GlobalErrorHandler implements ErrorHandler { // this method will receive the error from anywhere in our app and handle it handleError(error: unknown): void { // log the error console.error('An error occurred:', error); // perform other error-handling operations, like showing toast messages, // sending the error to analytics, and so on } } ``` And in the `app.config.ts` file: ```typescript export const config = { providers: [ { provide: ErrorHandler, useClass: GlobalErrorHandler }, ], }; ``` And that's it! Now, *any* uncaught error in our application will pass through the `handleError` method of our `GlobalErrorHandler` class. We can easily check it by adding a error in some random component: ```typescript @Component({ selector: 'app-root', template: `

Angular Error Handling

`, }) export class AppComponent { throwError() { // test with the click of the button throw new Error('This is a test error!'); } } ``` When we click the button, we should see the error logged in the console, and any other operations we added in the `handleError` method will be executed as well. Now, the fascinating thing about `ErrorHandler` is that it is not limited to just synchronous errors, but will also pass through async errors as well. We can see it by adding a simple `fetch` in some random component: ```typescript @Component({ selector: 'app-root', template: `

Angular Error Handling

`, }) export class AppComponent { fetchData() { // test with the click of the button fetch('http://some-wrong-api.com/wrong-endpoint'); } } ``` When we click the button, we will again see the `handleError` method in action. However, this is suitable for generic, global tasks, but async error handling often involves advanced and highly localized tasks, such as showing some default data, loading a different page depending on the context, and more. > Note: `ErrorHandler` will be triggered on *any* error within your app, for example, if an image failed to load, or some library threw and inconsequential error. So, filtering error types in the `handle` method is incredibly important! Now, everything we covered here relates to errors happening *inside* our Angular app; but what if there are errors from an outside context? Maybe there's a third party script running from the `index.html` file, or we could be using Angular Elements to incorporate our Angular components inside other applications and want to monitor errors that happen there. Until v20, it was not possible to catch those, but in v20, Angular is introducing a new config that allows us to catch errors that happen outside of Angular's context. We can simply add that command to our `app.config.ts` file: ```typescript export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), ] }; ``` This config will redirect error handling from browser's `window.onerror` and `window.onunhandledrejection` to Angular's `ErrorHandler`, which we covered previously. So, let's now explore different approaches to async error handling in Angular. ## HTTP Errors We will start by discussing HTTP as it is, without involving concepts like signals and resources; we will discuss them further down this article. ### Handling HTTP errors using RxJS capabilities Simply put, an HTTP request in an Angular app, if we, of course, utilize the `HttpClient`, is an Observable, so handling that sort of error would come down to error handling with RxJS. Let's start with the simplest example: ```typescript @Component({ selector: 'app-root', template: `

Angular Error Handling

`, }) export class AppComponent { constructor(private http: HttpClient) {} fetchData() { this.http.get('http://some-wrong-api.com/wrong-endpoint').subscribe({ next: (data) => console.log(data), error: (error) => console.error('Error fetching data:', error), }); } } ``` In this example, we are using just the `subscribe` method of the Observable to handle the error. The `subscribe` method can take not just one callback, but an `Observer` interface, which is an object that can have 3 methods: `next`, `error`, and `complete`. - `next`: this method is called when the Observables sends a new value, in our case, the HTTP response - `error`: this method is called when the Observable encounters an error, in our case, when the HTTP request fails - `complete`: this method is called when the Observable completes, which in our case is equivalent to HTTP success, so we don't have to implement it. However, this is probably the worst way to handle HTTP errors within an Angular app, as we know there are better ways to work with Observables. For instance, if we want to show the data we receive via HTTP in the UI, we might use the `async` pipe, at which point we won't have a `subscribe` callback to handle the error, and would instead opt to displaying something in the UI. This can be achieved via the `catchError` operator; what it does is, when an error occurs, it invokes the callback we provide which must return a different Observable, that will "replace" the original Observable. This can be used to return some sort of "error object" that can be used to handle the situation in the UI. Here is the same example, but using `catchError`: ```typescript import { catchError } from 'rxjs/operators'; import { of } from 'rxjs'; @Component({ selector: 'app-root', template: `

Angular Error Handling

@if (data$ | async; as data) { @if (data.error) {

Error: {{ data.error }}

} else {

Data: {{ data | json }}

} } `, }) export class AppComponent { private readonly http = inject(HttpClient); error: string | null = null; data$: Observable fetchData() { this.data$ = this.http .get('http://some-wrong-api.com/wrong-endpoint') .pipe( catchError((error) => { // notice the usage of the `of` function, // as `catchError` callback *must* return another Observable return of({error}); }) ); } } ``` As we can see, this is even simpler, and the process is way more streamlined: we do not subscribe to anything, we know we either have the `SomeData` object or an error object and use that fact in the template to display the appropriate UI. If your application is RxJS-heavy, which manby Angular apps are, you might be interested in streamlining the error handling process. This can be achieved, among other things, via utilizing [custom RxJS operators](https://rxjs.dev/guide/operators?ref=angularspace.com#creating-custom-operators). To learn more about handling error (and loading) states with custom RxJS code, you can check out [this article](https://www.angularspace.com/create-custom-rxjs-operators/) by [Eduard Krivanek](https://eduardkrivanek.com/?ref=angularspace.com), specifically the section 7. Now, we have explored how to handle HTTP errors with RxJS, but this is the most localized way of handling it; however, lots of cases of HTTP failures might require a more generic way of dealing with them, such as showing a toast message, or, in the case of some really low-priority requests, just logging some diagnostics and not showing anything to the user. Let's see how this is accomplished. ### Handling HTTP errors with interceptors If you thought "well, we can use interceptors to handle such scenarios", you've guessed it right! If you're unfamiliar with interceptors, check out [one of my previous articles](https://www.angularspace.com/magic-with-interceptors/) covering them in depth. So, with interceptors, we can easily catch errors and apply some generic handling logic: ```typescript export const errorInterceptor: HttpInterceptorFn = (req, next) => { const toastService = inject(ToastService); const diagnostics = inject(DiagnosticsService); return next(req).pipe( catchError((res) => { if (res.type === HttpEventType.Response && res instanceof HttpErrorResponse) { toastService.showError('An error occurred while fetching data!'); diagnostics.logError(res); return of(res); } }) ); }; ``` As we can see, we can use the `catchError` operator to catch the error and perform some operations, such as showing a toast message or logging the error to some diagnostics service. In the end, we just return the same response to also allow for local error handling (changing something in the UI) as we showed int he previous example. But this approach can be taken even further! What if we want to log the diagnostics always, but only show the toast message in around 80% of the cases? Well, we will need a way to understand which requests need to show the toast message and which ones don't. Our first instinct might be to add a custom header or some query parameter to the request, but Angular actually provides a built-in way to communicate with interceptors from your services known as `HttpContext`. This is a context object that can be passed to the request and can be used to store any data we want. This object is also available in the interceptor. So, let's see how we can use it: ```typescript // creating the token with data to pass with the request export const NoToastMessage = new HttpContextToken(() => true); @Injectable() export class SomeService { private readonly http = inject(HttpClient); getData() { return this.http.get('some.url', {context: NoToastMessage}); } } ``` Now, we can slightly modify our interceptor to account for this scenario: ```typescript export const errorInterceptor: HttpInterceptorFn = (req, next) => { const toastService = inject(ToastService); const diagnostics = inject(DiagnosticsService); return next(req).pipe( catchError((res) => { if (res.type === HttpEventType.Response && res instanceof HttpErrorResponse) { // log the diagnostics anyway diagnostics.logError(res); if (!req.context.get(NoToastMessage)) { // if there is not token, show the toast toastService.showError('An error occurred while fetching data!'); } return of(res); } }) ); }; ``` This way, our interceptor will have more granular control over how to handle a particular request, and not show the toast message unless we want to. Finally, there is one issue that can be covered when talking about interceptors in the context of error handling, and that is custom API error responses. See, sometimes certain APIs, instead of responding with familiar error codes like 404, 500, etc, respond with a custom error object. Something like 200 OK, but the body of the response is `{success: false, error: 'Some error'}`. Now, this is not a problem in and of itself, and the validity of this approach is out of the scope of this article, however, it creates a lot of boilerplate, since we essentially have to handle the error twice: check for a network error (which can still happen regardless of the server response), and then write an `if` statement which checks if the actual response is successful. However, with interceptors, we can avoid this, and do this checking once and raise an error so that it can be handled further down the line: ```typescript export const nestedErrorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( map(res => { if (res.type === HttpEventType.Response) { const body = res.body as {success: boolean, message?: string}; if (body.success === false) { throw new HttpErrorResponse({error: body.message}); } return res; } return res; }), ); } ``` This helps us remove a lot of friction when it comes to HTTP responses and different sorts of APIs we might encounter when building a frontend application. Now, as we touched all of these topics that were quite "old", we are now going to circle back to synchronous error handling, as we are about to cover the shiniest new feature of Angular 16+ - signals. ## Errors with Signals First, before we move forward, we must understand what we mean by "error with Signals". After all, signals are wrappers around values, and they are synchronous, so speaking about "errors within signals" makes as much sense as speaking about "errors with variables". And it is true that conventional signals (created with the `signal` function) are not capable of throwing errors. However, the situation gets more complicated when we start using signals that are `computed`. See, computed signals rely on callback functions that track other signals and execute to calculate the new value of the computed signal. This means that if an error occurs in the callback function, it will be thrown and can be caught. For instance, consider this piece of code: ```typescript @Component({/* */}) export class SomeComponent { count = signal(0); doubleCount = computed(() => { if (this.count() < 0) { throw new Error('Count cannot be negative!'); } return this.count() * 2; }); } ``` Now, if we set the value of `count` to a negative number, the computed signal will throw an error. But what does that mean? When will the error "happen"? To understand this, we need to go back to the basics and understand how computed signals work. Computed signals in their nature are *lazy*, which means Angular tries to prevent executing the callback function (which might be costly) as much as possible. For instance, the very first time the callback executes is *not* when the computed signal is created, but when it is first *read* (meaning when we "call" the computed signal, like `this.doubleCount()`) Now, when that callback executes for the first time, Angular takes note of what other signals are being used within it (in our case, the `count` signal), and make a list of them to "track" in order to be able to update the value of the computed signal when one of those tracked signals changes its value. However, here the signals also work in a lazy way, meaning when the value of a tracked signal changes, the callbacks does *not* re-execute immediately; instead, the computed signal is marked as "dirty", and the code moves on. But when this "dirty" computed signal is read again, Angular now knows its value might have changed, and re-executes the computation callback to get the new value. This is when the error happens. So, to put it in a few words, we can be sure that signal errors do not happen randomly at some point where we update some (other) signal, but when a computed signal is being read. This gives us some tools to help handle these errors. - If the computed signal throws an error because of some issue or edge case (like using some weird API, etc), it is usually easier to fix the issue rather than bother with error handling. - If we own the computed signal (meaning it's not coming from a library or some other code that we cannot change), we can read it in a `try`/`catch` block and handle the error gracefully. This is not very enjoyable, but is the only way to handle such an error - Finally, if we *do* own the code of the computed signal, and we anticipate some errors, it is best to just handle them within the callback function and return a default value. ```typescript @Component({/* */}) export class SomeComponent { // we are using some library we cannot change private readonly utilities = inject(UtilitiesService); count = signal(0); newCount = computed(() => { try { // we might expect an error coming from the library const newValue = this.utilities.calculateNewValue(this.count()); return newValue; } catch { // if the library throws an error, we can return some default return 0; // or some other default value } }); } ``` Now, this is way better than putting `try`/`catch` blocks all over the place. Note that this approach is also better because, more often than not, computed signals are created to be consumed in templates, and the Angular template syntax does not have an equivalent of the JavaScript `try`/`catch` statement. So, it would be a complex task to figure out how to handle an error in a computed signal that is being used in the UI. It is important to state that this approach is useful when we simply want to provide a fallback value if there's an error. If we want to perform a side-effect (like displaying a toast message as covered previously), we should fully forego the `computed` signal and instead use an `effect` to cover possible scenarios and courses of action. ```typescript @Component({/* */}) export class SomeComponent { private readonly utilities = inject(UtilitiesService); count = signal(0); newCount = signal(0); constructor() { effect(() => { try { const newValue = this.utilities.calculateNewValue(this.newCount()); } catch { // show a toast message } }); } } ``` However, we can go even further. If we want to return the previous value if there is an error, instead of using the `computed` function, we can opt for the new `linkedSignal` utility. With `linkedSignal`, we have access to the previous state, we can use it to keep the value and "swallow" the error: ```typescript @Component({/* */}) export class SomeComponent { private readonly utilities = inject(UtilitiesService); value = signal(0); computedValue = linkedSignal({ source: this.value, computation: (previous, current) => { try { const newValue = this.utilities.calculateNewValue(current.source); } catch { return previous; } } }).asReadonly(); } ``` Here, we take another signal and run a computation on it. If it succeeds, we have a new value, and if there's an error, we simply pick the previous value that `linkedSignal` provides for us, and return it, so the consumer is not even aware of the error. We also use the `asReadonly` method to make sure the signal is not writable, as if it were a simple computed property. > Note: This approach highly depends on what we are trying achieve, and more often than not it is wiser to not chew up the error and instead handle it in different ways. Now, the next way of getting an error in a signal pipeline is when creating a signal based on an Observable using the `toSignal` functions. In this case, however, we do not have an option of using the `try`/`catch` approach, and instead have to go back to the previous section of this article and handle the error within the RxJS pipeline using the `catchError` operator. We can use it in the same fashion as in the previous example, returning an "error object" that could be used in the future when reading the signal both in the template and component code. We can also blend it with the previous example with `linkedSignal` to keep the previous value of the observable-based signal in case of an error: ```typescript @Component({/* */}) export class SomeComponent { private readonly http = inject(HttpClient); rawData = toSignal(this.http.get('http://some-wrong-api.com/wrong-endpoint').pipe( catchError((error) => { // return an error object return of({error}); }) )); data = linkedSignal<{error: string} | SomeData, SomeData>({ source: this.rawData, computation: (previous, current) => { if (current.error) { return previous; // return the previous value } return current; // return the new value } }).asReadonly(); } ``` Finally, we are arriving at the end of this article, and are about to cover HTTP calls again; this is because of the new tools introduced by Angular that allow us to perform HTTP calls in a reactive fashion and using signals, namely, the Resource API. ## Errors with Resources First, we should make clear that since all the types of resources (`resource`, `rxResource` and `httpResource`) have the same API signatures, especially when it comes to errors, we will simply cover the `httpResource` scenario and the rest will be similar. If you're unfamiliar with the Resource API, you can check [my previous article](https://www.angularspace.com/meet-http-resource/) where we dive deep into this topic. Now, let's see how we can handle an HTTP error with the `httpResource` API. Basically, we have two scenarios: either we display some fallback UI, or we perform some sort of side-effect (again, like showing a toast message). The first scenario is as easy as it gets: ```typescript @Component({ selector: 'app-root', template: ` @if (data.hasValue()) { } @else if (data.hasError()) { } `, }) export class SomeComponent { data = httpResource(() => 'http://some-wrong-api.com/wrong-endpoint'); } ``` Here, we make use of the `hasError` method to determine if an error happened previously, and display the UI accordingly. But what about side-effects? Well, the `hasError` method can be tracked by `effect`, so we can write a very simple effect to determine if something needs to be done: ```typescript @Component({...}) export class SomeComponent { data = httpResource(() => 'http://some-wrong-api.com/wrong-endpoint'); constructor() { effect(() => { if (this.data.hasError()) { // show a toast message or something else } }); } } ``` So, with the Resource API, we now can easily handle errors in a very simple and straightforward way, both in the template and in the TypeScript code, as opposed to computed signals and signals created from other Observables. ## Conclusion As we saw, the topic of error handling in Angular is robust and full of different precarious scenarios. However, we also see that Angular equips us with the best tools to help handle such scenarios, and makes us remember that what separates a poorly-designed application from a great product is the ability to adapt to different user scenarios. ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) P.S If you want to learn about error handling and other scenarios with Signals, check out the 6th and 7th chapters of my book ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/06/Screenshot-2024-11-13-at-15--1---1-.jpg) ### Angular + Rome = NG ROME 2025 Tickets Giveaway :) URL: https://www.angularspace.com/angular-rome-ng-rome-2025-tickets-giveaway/ Last updated: 2025-05-28T10:24:20.000Z Thanks to the courtesy of NG Rome Conf organizers I can offer you: **\- 2x FREE** NG ROME Conf Tickets! ( Raffle ) 🏆 _This post is for subscribers only._ ### You're misunderstanding DDD in Angular (and Frontend) URL: https://www.angularspace.com/youre-misunderstanding-ddd-in-angular-and-frontend/ Last updated: 2025-05-05T22:39:51.000Z ## Motivation In the recent year or two I've seen lots of discussions around building Domain-Driven Design in Angular applications. My observation is that people discuss things that are orthogonal to DDD at best - or even totally derailed from what DDD is about at its core. Participants who attended my Angular and/or Architecture [trainings](https://ducin.dev/?ref=angularspace.com) or who came to my [NG-Poland](https://ng-poland.pl/?ref=angularspace.com) workshop, or simply people whom I meet here or there (frontend developers, but mostly from Angular community) all seem to discuss DDD/Angular in terms of specific tooling and specific structure of the application. Also, I see posts in social media such as this one: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/image.png) Obviously there's absolutely nothing wrong with asking questions (*I'd rather have questions that can't be answered than answers that can't be questioned*). However, all this shows that the wide Frontend/Angular community has little idea on why/when/how would you even approach Domain-Driven Design. All in all, there's a growing [semantic diffusion](https://martinfowler.com/bliki/SemanticDiffusion.html?ref=angularspace.com) within frontend communities about DDD and it's bringing more harm than good (in short, *semantic diffusion* happens when people use a term without understanding it and, within some time, the meaning gets derailed ☹️). That's what I want to address in this post 🤓. # Business Context Example In order to illustrate DDD concepts in a more approachable way, let's assume that we're working on a system that allows patients to schedule appointments with doctors, specialists, etc. to deal with medical issues. Putting it very simple, we could say that we're building a **medical appointment application**. We'll stick to examples from this domain. # DDD is abstract and difficult DDD is a methodology that's rather easy to cover just the main topics and building blocks (see: *DDD at a glance* section below). But **it takes many years of hard work** to gather the experience (not just theoretical knowledge) to be able to **use it in practice** for the **benefit of your product**. It's difficult to get the abstract concepts right and then to [reificate](https://dictionary.cambridge.org/dictionary/english/reification?ref=angularspace.com) them in your business. The main problem with DDD is that people stick to a shallow level of it, yet they label it as "*doing DDD*". Mostly, it's taking the code/organization you have right here right now - and labelling them somehow according to DDD's terminology 🤦. Thankfully, not all people do that! As a result, on one hand, DDD helped enormous and complex businesses "tackle their complexity" and scale well, it even enabled them to grow in different areas. On the other hand, for other companies, it turned out to be a failed promise which turned out to be a wasted effort, time and money. And a lack of trust to any DDD-related thingies in the future. (and that's where my pain point is). DDD is a tool. In hands of competent people, it could be very helpful. And vice-versa. # DDDD: Derailed Domain-Driven Design There's a whole legion of frontend devs who think that organizing your code in some **modules** and automating tooling around **monorepos** is "*doing DDD in Angular*". (we'll get back to monorepos later 😁) Couldn't be farther from the truth. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/one-does-not-simply-ddd-angular.jpg) DDD is an approach you **do within your product, a business segment, organization-wide**. But **not separately on Frontend**. One could say (simplifying the topic quite a bit) that it's when the business semantics drive technical boundaries. But these technical boundaries include all the stack: Frontend, Backend, Database(s) - and activities: Deployment, Monitoring/Observability (DevOps) and so on, and so forth. 🔥 **You don't do DDD on Frontend in separation, as your frontend is not a separate product**. 🔥 Frontend is not a separate "module" either. If you feel this sounds like blasphemy, then please get familiar with the [C4 model for documenting architecture](https://c4model.com/?ref=angularspace.com) by Simon Brown. It's a bigger topic, and there are quite some variations of that model, but what's important for us here is that: - **C3 - Components** \- is a layer of logical architecture, i.e. it defines logical dependencies between modules, e.g. when a customer is charged, the system requires the card information from another place in the system - **C2 - Containers** \- is a layer of deployment architecture, i.e. it defines deployable artifacts (binaries, bundles, etc.) ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/image-1.png) Yes, most often you *deploy* frontend separately from backend (C2). But most often, a *business module* (*Bounded Context*, discussed later in the article) consists of Frontend, Backend and Database(s) altogether (C3). So you don't want to detach frontend's capabilities from its backend counterparts. They **achieve a business goal as a whole**. If you design a model (i.e. data structures with their semantics) then some parts of it (specific interfaces) are being shared between the server and the client - that's a contract ([DTOs, Data Transfer Objects](https://en.wikipedia.org/wiki/Data%5Ftransfer%5Fobject?ref=angularspace.com)), often automated via [Swagger/OpenAPI](https://swagger.io/?ref=angularspace.com) or [Pact](https://pact.io/?ref=angularspace.com). ## So you can't do DDD on Frontend? No, don't get me wrong 😉 you **can** of course apply DDD to your Frontend. But... that means that there were **product-wide considerations about modularity** (*Bounded Contexts*), about dependencies across these Contexts (illustrated within *Context Maps*, discussed later). # So what DDD is about? Let me say it again: DDD is the approach where we focus on **OUR PRODUCT** and **UNDERSTANDING THE BUSINESS**. That's where we start our activities. And that's where we make sure that what we did actually makes sense. And it's the business (not the tech) is what makes us iterate over our solutions. If you're thinking in terms of DDD from technical perspective, **YOU'RE ULTIMATELY DOING IT WRONG**. Very wrong. What does that mean in practice? Well, first of all, there is no place for you in DDD if you're focused **mainly** on your beloved framework, beloved language, technology or whatever else. In DDD, your main focus is **THE PRODUCT**. That means you're actively interested in understanding: - How does **your product make revenue**? What factors can increase or decrease the revenue? - In our medical case: do we charge individual patients or do we sell some healthcare packages for companies and we charge a whole company for multiple individuals? - What is the **service that the customer buys**? - Do you charge patients per appointment or is there a subscription plan or anything else? - What is the **value that is brought to your customer**? How do they benefit from it? - Are these simply a one-time appointment such as teeth extraction or maybe a long-term process, including lots of appointments, such as cancer treatment? - How do you **charge your customers**? - Single appointment, subscription plan, periodically? - How does the **customer use your product**? What makes it a pleasant experience or what is an obstacle that could be removed? Do you talk to your customers directly or do you just receive a Jira ticket? - How CRUDy your interface is - or how well does the UI express the business process for the user ([CRUD-based or task-based UIs](https://www.youtube.com/watch?v=DjZepWrAKzM&ref=angularspace.com))? - How does your company take advantage on the market as compared to the competition? In other words, which parts of the system should you improve to build competitive advantage: a CRUD-based module for internal employees, or an experimental customer-facing UI? - Web UI for a medical app is nothing special. So is a mobile app. What compelling business feature would make an individual consider switching from one medical company to another one? - Here you're seeking for "*Genesis*" section of [Wardley Maps](https://www.wardleymaps.com/?ref=angularspace.com). - What are your **business KPIs**? Do you track them? Do you even care about them? - An example business KPI for a [**stream-aligned team**](https://teamtopologies.com/?ref=angularspace.com) would be to maximize the consumption of the less popular doctors availability so their services are utilized better. - Another business KPI would be to reduce the amount of patient appointments that are being cancelled in the very last moment, which is expensive for our business. - Are there any **scarce resources** within your business that distinct customers are competing for? In other words, if customer A gets a thing, does it mean that customer B cannot get the very same thing? Is there a limited number of these items or services in time, in a period of time, in quantity, in any other way? - In a medical center system, the scarce resource is the availability of doctors and the clinics. If a specific time/space slot is allocated, nobody else can use it: First come, first served. And *many, many, maaany more*. # DDD at a glance At this point you might feel a little bit concerned: why am I *not* speaking much about **subdomains** and **bounded contexts** and all these other popular DDD (buzz)words? The truth is, these are just **formalisms**. And they are not the essence of the problem that DDD aims to solve. They're just some specialized terminology that helps us to communicate faster, they form a **mental model / framework** which we can build our understanding on top of. So let me outline the basic concepts of DDD in very simple words... ## Strategic vs Tactical DDD First, there is **Strategic DDD** and **Tactical DDD**. The **Strategic** one deals with the business mechanics, the business capabilities, business opportunities, business problems and so on. While the **Tactical** one deals with technological aspects, essentially: how to code these things And make them work correctly in terms of both scale and business complexity. Strategic DDD is split in two parts: the **Problem Space** and the **Solution Space**. ## Domains and Subdomains We haven't yet covered the first "D". A **"domain"** is the area in which a business operates. For an online shop, it would probably be **e-commerce**. For a company that delivers packages, that would be **logistics** or **spedition**. In case of our medical app that might be **medical care**. But don't you have a feeling, that "medical care" is somehow broad, includes lots of various concerns? You're right! In order to schedule a patient's appointment to a specialized doctor, we need to cover the following: - What **doctors** do we have at hand? - In which **clinic** or other **location** would the **appointment** take place? - At what exact date and time? i.e. when are both the doctor and the clinic available and does the patient **match this availability**? - How is the patient going to **pay for the service**? - Are there any **referral programs** so that the patient could **invite other patients** to our clinic? - Do we **track the history** of the patient's appointments? If we do, how do we exactly use the data? How can we benefit from it? - How do we **communicate** with our patients? - How do we deal with **unexpected situations**, e.g. the doctor is not available? The scheduled appointments need to be either cancelled or rescheduled. That's a whole lot of problems that our business need to figure out and have it under control. That might be digitalized within an IT system, but it could also be manually managed by employees (see: [sociotechnical systems](https://en.wikipedia.org/wiki/Sociotechnical%5Fsystem?ref=angularspace.com)). All above questions to be answered could be treated as subparts of the main domain, hence the name: "**subdomains**". Domains and subdomains are parts of the "**Problem Space**". That's not something that we can design. That's how our business operates in the real world. We discover it. ## Bounded Contexts However, we can design how do we want to tackle these questions and problems. We apply the "[Divide and Conquer" approach](https://en.wikipedia.org/wiki/Divide-and-conquer%5Falgorithm?ref=angularspace.com) to split a big problem into smaller, simpler problems. We break our entire system into smaller, more autonomous modules called "**Bounded Contexts**". At this point we **don't focus on the technology**. We don't care much whether it's going to be a frontend monolith or MicroFrontends, whether we're going to use monorepos or multiple repos. Properly shaped Bounded Contexts would fit into any deployment architecture. It's our decision, how do we split, and how many bounded context we would have. There is, theoretically, an infinite number of viable solutions. Getting Bounded Contexts right is absolutely **the essence** of what we do in DDD. And what's very often the source of failed attempts to DDD: jumping straight into tactical phase (implementation) without thinking thoroughly the strategy. So how do we **identify** Bounded Contexts? That's where "**Domain Experts**" come into play. If you're developing a "product" (i.e. you're not just grabbing and fixing Jira tickets), then there must be some people in your company who know how the business operates. These might be analysts, specialists, product owners or even directors. It doesn't really matter what is their official position within the structure. What matters is that they understand the business. ## Context Maps Bounded Contexts try to be autonomous, but sometimes they have to communicate with each other. We try to keep the amount of communication to the necessary minimum. From a high-level perspective, architects need to get the context depencencies under some control - that's where **Context Maps** come into play, [along with their patterns, such as: *Anti-Corruption Layer*, *Open Host Service*, *Shared Kernel* and so on)](https://github.com/ddd-crew/context-mapping?ref=angularspace.com). Below is an exemplary, simplified Context Map. It outlines the boundaries across contexts: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/image-2.png) Note that this is in **NO WAY UNIVERSAL** map for any medical application. I made quite some assumptions on how business works. Each company would probably have a totally different Context Map, based not only on their unique system, but also the business processes and the business rules that drive them. Also note, that there's no 1:1 correlation between Subdomains and Bounded Contexts. Or at least, there doesn't have to be. Again, that's a deeper topic for another article. But what **matters a lot right now** is that: ## Frontend is NOT a separate Bounded Context. **Frontend layer just fits into the Context Map**. So you'd rather do Angular within a DDD-oriented product, than DDD inside an Angular application. A single context could have both backend and frontend (e.g. Appointments Search Context), but it could also have backend only (SMS/Email Notifications, e.g. communicating via kafka messaging, whatever) - it depends on what responsibility it has. A given piece of UI might need to integrate with a legacy backend system - or even with an external backend (e.g. the one processing actual payments, or a SMS/Mailing service), sure. But quite often there is (or at least there should be) some backend façade / BFF on your side that communicates with that legacy/external system, which would take care of concerns such as making sure the patient doesn't get the same messages multiple times (even if there are network or system failures). No matter what's the exact situation - **you draw module boundaries from a high-level perspective**, and not on frontend-level only. However, frontend is never a separate *Bounded Context*. And that's quite a popular misunderstanding. And quite often a reason for devs inventing frontend-DDD, while searching for aggregates in frontend landscape (spoiler: they won't find it, as Frontend doesn't deal with persistence). For instance, they tend to view HTML forms handling as DDD aggregates, which doesn't make much sense, as client-side code handles HTTP/WebSockets communication, but not persistence/consistency/transactions. And even if you're using SSR tech such as [Next.js which allows to process forms via server actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations?ref=angularspace.com), then yeah - you *might* have an aggregate on the server-side, **but not on the client**. And we go back to the starting point 🥹 As you see, DDD is tech-agnostic. ## Ubiquitous Language and Semantics When people communicate, while working on a product with complex domain, they need to exchange quite complex thoughts. Specialized terminology comes into play. You can think of *Ubiquitous Language* as a dictionary, potentially written down somewhere - or commonly understood within the company - which defines precise terms. For instance, a *specialist* is any professional who can perform *medical services* for *patients*. A specialist could be, first and foremost, a "doctor", but not only: "nurses", "technicians" and many other professions are also included here. Explicitly, we don't use "specialists" or "doctors" **interchangeably -** **not to introduce confusion**. A specialist in our case is a specialist, period. *Ubiquitous Language* is ubiquitous in a way that it appears in **conversations, in code, in tests** etc. But it's **NOT** ubiquitous in a way that it appears across the whole company - it's specific to a specific Bounded Context. Why? Imagine we've got B2B and B2C services. They're being handled in totally different ways. Now let's consider who a *Customer* is: is it an individual (B2C) or a company (B2B)? Does it make sense to create a **Unified, Universal Model** (also called [Canonical Data Model](https://en.wikipedia.org/wiki/Canonical%5Fmodel?ref=angularspace.com)) of a *Customer* that would be used throughout the whole application? That deserves a separate article (let me know if you want that), but clearly: **HELL NO!** So what's the relation between a Customer and a Patient? Do these two models exist side by side in a single context or are they exclusive to separate contexts? Where does it actually make sense to use "Patient" and when to use "Customer"? Patient for Appointment Search, Scheduling, etc. and Customer for Billing, Invoicing etc - or something else? Customer should have a TAX ID. The Patient should have a Social Security Number (or whatever it's called in your country). Does TAX ID matter when a patient schedules an appointment? Does SSN matter when billing? How do we draw the boundaries between these models? **🔥 That's DDD. 🔥 That's the heart of it. 🔥** How much is it related to Nx Monorepos, NGRX glocal or component Store, Signals, services in this style or another? It's just not. You can think of Semantics as the first and foundational step in learning DDD. And getting detached from what your database/client-store data model is. By the way, speaking very formally: > **Bounded Context is a SEMANTIC BOUNDARY where a Ubiquitous Language makes sense.** 😅 A term/word, outside of a Bounded Context, has totally different meaning. So who is a *patient*? In terms of appointments, it's someone who utilizes our resources. [Sad but true](https://www.youtube.com/watch?v=TpohVYomw2o&ref=angularspace.com) 😅 In terms of billing, it's someone who pays for our services (directly, or indirectly) In terms of referral programs, it's someone who brings new customers (and revenue). In terms of unexpected situations, a patient is a "problem", as we have to reschedule their appointments if the doctor unexpectedly becomes unavailable. And, probably, we're not automating this fallback process, as that would hit UX significantly (would you like a machine to reschedule your visit without asking you?) And so on. Each Bounded Context is "enclosed" by specific semantics. In various Bounded Contexts you might have ffew, several or even tens of different representations of what the *Patient* is. That's normal. (that's also, by the way, a reason why you might NOT want to use any centralized state management solution on Frontend, as they tend to [normalize client state](https://en.wikipedia.org/wiki/Database%5Fnormalization?ref=angularspace.com), which breaks autonomy across modules. In short: **integrate modules via EVENTS, not via STATE** \- but that's yet another topic for a separate article). ## Tactical DDD on Frontend? No, thank you Of course, there's a whole separate part: the Tactical DDD. In very short, it deals with specific design patterns that we apply to implement DDD. This again, deserves separate articles, but just to put it short: - **Entities** \- define business objects that change over time, so they have their identities (IDs). - Examples: specialist, patient, customer. - **Value Objects** \- immutable data **values** - Examples: price, visit duration. - **Aggregates** \- the most important one. It's **NOT** a graph of related objects, as many explain it. It's a **consistency boundary** for **concurrent operations** to take place. Sounds complex, isn't it? Yep, you determine one's skills in DDD by looking at their aggregates. - Example: few patients want to schedule an appointment on the same date/time, same specialist, same place etc. Only one can get it. - What matters: concurrent access to a resource, and that the resource is scarce (there might be a race for obtaining that resource between users/parties) - Would there be a problem, if multiple patients would schedule the same specialist/clinic/time slot? If there is a problem, how big of a business problem is it? Can we accept that in small amounts (and what does a "small amount" mean?) - Technically, think of an aggregate as the ***smallest*** possible chunk of data that allows you to handle race conditions. You might need to federate your database or shard it (usually, complex domain systems come with big scale, i.e. horizontal scaling). The aggregate technically uses some way of database transactions to ensure that no business rule is broken. - Quite mouthful, isn't it? Told ya, it's not that simple... And, oh, by the way, why Tactical DDD doesn't make sense on Frontend? You're right - aggregates don't make sense on clients, since they *almost* never handle database/persistence/concurrency directly. - Very common mistake: aggregates are too big or absolutely way too big, which makes them perform bad, and also enforce unnecessary data integration (data consistency) for data which is totally not needed there. - Another common mistake: there's no concurrent access for a resource, e.g. we collect patients' feedback. Feedback of one patient has no concurrency/race conditions/etc. with another patient's feedback. Hence, *you might not need an aggregate* in many cases. - **Repositories** \- they extract the data from, most probably, RDBMS, and build a domain-friendly tree of objects, often as an aggregate. What's important, **the ORM/DB model could be quite different from your domain model**. A repository is not meant to do the DB call directly, probably it uses some technical layer underneath. - There are more tactical building blocks, but I'm sure it's enough for now 😇 These patterns are critical in some Bounded Contexts on backend (not all though - it's perfectly fine if some backend parts are simple CRUDs if that's what fits). However... ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/one-does-not-tactical-ddd-frontend.jpg) In 99,99% cases, that'd be pure [Accidental Complexity](https://khalilstemmler.com/articles/software-professionalism/absolute-and-relative-complexity/?ref=angularspace.com). You don't want that. By the way, if you name your Angular Services performing HTTP requests as "Repositories", you make your code look smarter than it actually is. That's not technically wrong, but most probably it's semantically wrong, as the JSONs you fetch from the server (contract DTOs) should be already shaped in a client-friendly way. And the point of a Repository is to adjust the server's model to your domain model. That should be done on backend, not frontend. Don't stick to fancy names just for the sake of it - it doesn't help anyone. ## DDD Terminology Summary After wrapping up the DDD terminology, we could come up with a diagram similar to the following: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/ddd-at-a-glance.png) Lovely diagram, isn't it? Now you can say you understand DDD, right? ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/for-the-better-right.jpg) Unfortunately, **THIS DIAGRAM DOESN'T HELP YOU** much in getting the point of DDD. You can also refer to the original diagram included in [*the Blue Book*](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/B001JDYE0O?ref=angularspace.com): ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/ddd-terminology.webp) (sidenote: it doesn't help much in getting to the core of DDD either - BUT 😅 - if you like books, start with [*the Red Book*](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577?ref=angularspace.com) instead of *the Blue one*, as it's way more approachable) A wise person once said: > There's a big difference between knowing a word and understanding its meaning. I could only add that, from my observations, the difference is really huge. And it's often the reason for the *semantic diffusion*. People use a term without understanding what it is. It diverges from its origin. And hence, when someone uses *the term*, what's the understanding of the person using it 😕. At this point, if you think that using DDD's terminology here and there, **without some additional effort**, DOESN'T MAKE THINGS ANYHOW SIMPLER - then I'm absolutely happy. Don't rush to claim *you're doing DDD*. It's better to think in terms of being **business-driven** (not a buzzword yet), **business-centric** or **business-aligned**. Are you? ## Monorepos has nothing to do with DDD Having understood the basic concepts behind DDD - let's get back to... Monorepos. When it comes to monorepos, how you organize your code in directories/repos is purely orthogonal to your business/domain concerns - although it could affect team efficiency (*development and operations* \- yep, DevOps is NOT about docker or k8s 😅). But bear in mind, splitting your business codebase into *Bounded Contexts* aims to make them **autonomous**, independent on each other. And the whole point of using monorepos is to **make sharing easier** at a bigger scale, i.e. without CI/CD delay penalty, without long build queues etc. But, again, **sharing reduces autonomy**. The more you share, the less autonomous your team/module/context is. **Share with care**. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/sharing-vs-autonomy.jpg) **Monorepos are double-edged swords.** It's wonderful that monorepo tooling allow us to limit who can import what (e.g. using [Nx Module Boundaries](https://nx.dev/features/enforce-module-boundaries?ref=angularspace.com) or [ESLint Restricted Imports](https://eslint.org/docs/latest/rules/no-restricted-imports?ref=angularspace.com)). But let's quickly reframe the problem... Why would you introduce module boundaries/restricted imports? Hm, because people tend to import too much what they shall never import? Hm, if you didn't put entire codebase (many modules from many teams) into the single repository, **would the problem exist at all**? 😜 From my personal experience (I do lots of consultancy and trainings), in majority of cases, people try to share too much. And they **have to** reach out to solve problems they introduced themselves. I've seen frontend monorepos abused (used unnecessarily or misused) more often than used effectively and having a good reason. Not to mention CV-driven development. Though, there might be people strongly disagreeing with the statement - and we can never come to a conclusion unless we analyze specific case studies. **DISCLAIMER**: Having written all that, there's nothing wrong with monorepos. It's just a tool. But as each tool, use it with care. Don't make it a default choice blindly. ## Component Library Yes, you might need to share a **reusable component library**. However, there's no silver bullet, no universal solution. "It depends" on so many factors. If someone is trying to sell "*a universal solution that fits everywhere"* \- run away. You'll thank me later. There are multiple ways for sharing UI libraries. It could be a monorepo, but it doesn't have to be. Component Libraries are the #1 Frontend problem in big systems I've audited. And the #1 reason for it, it's because people try to put too much there. Again, topic for a separate article. If you disagree with what I wrote above, please refer to [Thinnest Viable Platform on TeamTopologies.com](https://teamtopologies.com/key-concepts?ref=angularspace.com). **You want your platforms to be small, not big**. But let's get back to the main topic... # When can we call it DDD? Finally, there's a quote from Eric Evans that I need to address: > \[...\] the most fundamental pattern of Domain-driven Design is probably the ubiquitous language. \[...\] > > \[A model\] applies within a certain context, and that context has a definitely defined limit, \[it's\] a bounded context. > > With those two ingredients, I would say, someone is doing Domain-Driven Design, and there are a lot of other practices that help solve more specific problems. The quote is being used as a *universal justification* for "doing DDD" if only you have **ubiquitous language** and **bounded contexts**. I hope at this point we have well understood that: - Frontend is not a separate Bounded Context - DDD is a product/organization-wide discipline - Frontend-only DDD is an architectural delusion - **you can do DDD on Frontend, if you're doing it throughout your entire product**. Also, note that... while using the above quote *and* while ignoring the **semantics** of what ubiquitous language and bounded contexts are, you could justify labelling pretty much anything as DDD. Feel free to call *any context* a "bounded" context. Same with ubiquitous language. Quod erat demonstrandum. Does it help anyone in achieving their goals? I strongly doubt that. Does it sound fancy? Probably... Unless you encounter someone who really understands the topic - then you might look like... someone really incompetent. 🔥 **The true mastery is when you deeply understand a topic - yet you use simple words, that are approachable by majority.** 🔥 No need for fancy words, if they don't bring measurable benefits. ## Community Reception And you know what? Experienced *backend/DDD* folks would likely summarize the article as: "*ah, that's kind of obvious, nothing new to me"*. Because it is 🤷. And yet, all above narrative might make some *frontend* folks' heads explode, as it ruins what they considered DDD to be. That's how *semantic diffusion* grows. # Wrap up As a summary - let's get back to the initial post and go through the questions: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/how-to-angular-post.png) Note the question: > *I want to start a new project in #Angular using Domain Driven Design.* ***Why?*** Do you have a complex *domain*? Do you have *Domain Experts* at hand? How would you draw Bounded Contexts? Also, "DDD in Angular" - but what about the rest of the system? DDD is not an architecture style, like modular monoliths, onions, hexagonal, etc. DDD could fit those for better or worse. When a seasoned DDD practitioner reads "*I want to start a new project using Domain Driven Design*", following questions pops up: - new project? Mkay, but how well do you know the domain? How likely is the domain concerns to cause trouble? Do you even know where the difficult part of your system is? - or in other words: if it's a new project, how do you know you already need DDD? DDD is not something you can "setup initially". > *What are your thoughts/opinions about DDD in big projects?* What is your application doing? DDD is not a matter of taste. Or trends. It's a tool. It's not a matter of being big or small either. It's a matter of your domain being complex - or not. It's a matter of how much you can or cannot rely on "simple CRUDs"... or how many business processes you've got... or how many people work on the system in parallel, and how much collaboration they need on a daily basis. And what do they have to agree upon (for the sake of system integration, yet keeping coupling at a minimum). > *Is it worth it?* As with everything, some will say "yes", some will say "no". It depends on what you need, what's your context, what's your architectural expertise and so on. Thankfully, there are tons of experts in the area we can all learn from. If you're interested in learning DDD, feel free to [reach out to me](https://ducin.dev/?ref=angularspace.com), or [follow any of people I consider experts in the field](https://github.com/ducin/awesomes?tab=readme-ov-file&ref=angularspace.com#people). You might also like to check out [my aDDDvent series](https://ducin.dev/ddd-introduction-frontend?ref=angularspace.com). Enjoy learning DDD! --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/Screenshot-2024-11-13-at-15.52.00--1-.jpg) ### Build A Full-Stack Application With AnalogJS URL: https://www.angularspace.com/build-a-full-stack-application-with-analogjs/ Last updated: 2025-05-02T07:00:20.000Z You’ve probably heard the Vue crowd singing praises about Nuxt, and the React folks wouldn’t dare start a project without Next.js. Meanwhile, over in Angular land, we were kinda missing out on that meta-framework magic. Or we did, until [Brandon Roberts](https://github.com/brandonroberts?ref=angularspace.com) stepped in with [AnalogJS](https://analogjs.org/?ref=angularspace.com), attempting to bring those same modern features to Angular. I’d heard about AnalogJS a bunch of times, so as an Angular developer, I had to give it a shot. I rebuilt my personal website, [eduardkrivanek.com](https://eduardkrivanek.com/?ref=angularspace.com) (shameless plug), switching it from React 😱 to Angular 😎. But slapping together a blog-style app wasn’t enough. I wanted to see if Analog could handle the demands of a full-stack application. In this post, I’ll walk you through some of the features Analog offers by building an anime search app. Think of this article as a summary of what AnalogJS is capable of. I will go over how I created this small application, but I highly recommend reading the [official docs](https://analogjs.org/docs?ref=angularspace.com). The presented project is available [on GitHub](https://github.com/krivanek06/analogjs-example-project?ref=angularspace.com). This example uses Angular 19.2 and AnalogJS 1.15.1\. Here is GIF about the final result that we will be. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/full-stack-analog-application-overview.gif) Overview Of The Application ## Getting Started So, what even is AnalogJS, and why should you care? Well, I’d say it’s worth checking out if: 1. You’re building a full-stack app and don’t want separate backend-frontend 2. You’re aiming for SSR/SSG-heavy pages and want that SEO optimization The features which Analog provides (and what I’ll cover throughout this post) are: - File-based routing - Route metadata - API routes - SSR + SSG rendering - Form Actions - Vite (instead of Angular CLI) - Markdown content support (like Nuxt Content) - Full-stack capabilities (API + frontend in one) To get started with Analog use the command `npm create analog@latest` or if you already have an existing project, and want to migrate to analog, you can follow their [migration guide](https://analogjs.org/docs/guides/migrating?ref=angularspace.com). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/full-stack-analog-application-create-project.png) Getting Started For my example you will see that I opted-out from [Analog’s SFC](https://analogjs.org/docs/experimental/sfc?ref=angularspace.com), which is the “svelte-like” syntax, however this is all personal preference. Another optional thing, in new projects, is removing `zonje.js` following [Angular’s guideline](https://angular.dev/guide/experimental/zoneless?ref=angularspace.com) for the best performance. ## Application Routing Overview The example app makes use of all the routing features Analog offers, including static routes, dynamic routes, layout routes, and route groups. We’ll break each of these down in more detail later, but for now, here’s the folder structure we’re working with: ```markdown src/ └── app/ └── pages/ |── (anime)/ │ ├── details.[animeId].page.ts │ ├── my-list.page.ts ├── (auth)/ │ ├── login.page.ts │ ├── login.server.ts │ ├── register.page.ts │ └── register.server.ts ├── (blog)/ │ └── [slug].page.ts ├── (auth).page.ts ├── [...page-not-found].page.ts ├── index.page.ts ``` First we start with the main `index.page.ts` that is served on the initial `/` route. Next, if the user somehow wanders into a non-existing route, the `[...page-not-found].page.ts` component will catch all of them. It represents a wildcard `**` route. For authentication, we’ve got `login` and `register` pages living inside the `(auth)` folder. This folder doesn’t change the actual routing, it’s just there to keep things together. We also added `(auth).page.ts` to provide a shared layout for those two pages. The `login.server.ts` and `register.server.ts` files are where form actions happen, but more on those later. Our blog section is handled by `(blog)/[slug].page.ts`, which maps to URLs like `/blog1`, `/blog2`, and so on. It’s a dynamic route used for rendering content pages. When someone searches and clicks on an anime, the details appear at `/details/123`, with `123` being the anime’s ID. This page is public and server-side rendered. Logged-in users can also create their own favorites list at `/my-list`. Since that’s a private page, it’s rendered entirely on the client side. ## Application Routing Detailed With this section I want to go a bit deeper how each route works, reading route parameters, how to configure rendering and setting metadata. ### Static Routes A route is considered static if there are no square brackets `[]` in the filename. In our case, the `/` and `/my-list` routes are static and look something like this (with a richer template): ```typescript @Component({ template: `

Home Page

`, }) export default class HomePageComponent {} ``` What I learnt is that selectors (`selector: 'app-home'`) have no effect on the page components, you can skip them and the component name is also irrelevant. We are using the `HomePageComponent` name, however you can name it `BananaComponent` and the final result will stay the same. What actually matters is where the file lives inside the `/app/pages` folder structure. Worth to mention to not forget the `default` keyword when defining a route, otherwise you can get the `NG04014: Invalid configuration of route ''` error. ### Dynamic Routes Dynamic routes are defined by their filename, where the route param is inside the square brackets. In our example, when a user navigates to `/details/123`, the `details.[animeId].page.ts` component kicks in and loads data based on the `animeId` value. ```typescript @Component({ template: `...` }) export default class AnimeDetailsPageComponent { private readonly route = inject(ActivatedRoute); private readonly animeApiService = inject(AnimeApiService); readonly animeDetails = rxResource({ loader: () => this.route.paramMap.pipe( map(params => params.get('animeId')), filter((animeId): animeId is string => !!animeId), switchMap(animeId => this.animeApiService.getAnimeById(animeId)) ), }); } ``` ### Layout Routes Layout routes are created by setting up a parent file and a child folder with the same name. In our case, we have the `(auth).page.ts` parent component, and inside the `(auth)` folder, we have the `login.page.ts` and `register.page.ts` pages as its children. To make the layout work, the parent `(auth).page.ts` needs to include a `router-outlet` to render its child routes, like this: ```typescript @Component({ imports: [RouterOutlet, RouterLink], template: `

Auth Layout

`, }) export default class AuthLayoutComponent {} ``` ### Catch-All Routes This route was defined by `[...page-not-found].page.ts`, as it represents the wildcard `**`. The following example is from the [official docs](https://analogjs.org/docs/features/routing/overview?ref=angularspace.com#catch-all-routes): ```typescript import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { injectResponse } from '@analogjs/router/tokens'; import { RouteMeta } from '@analogjs/router'; export const routeMeta: RouteMeta = { title: 'Page Not Found', canActivate: [() => { const response = injectResponse(); if (import.meta.env.SSR && response) { response.statusCode = 404; response.end(); } return true; }], }; @Component({ imports: [RouterLink], template: `
Go Back Home`, }) export default class PageNotFoundComponent {} ``` The `routeMeta` is set up so that if you type a non-existent URL like `/nonexistingroute` and hit enter, the request goes through SSR by default. But thanks to `routeMeta`, the server won’t even try to render that page, it just returns a 404 right away. However, if you navigate to a non-existing route from within the app (like via a button click), it falls back to client-side rendering and shows a friendly "go back home" message instead. ### Route Metadata Another great use case for [route metadata](https://analogjs.org/docs/features/routing/metadata?ref=angularspace.com) is SEO and optimizing how your pages appear on social platforms. If you're new to that, the [MDN guide on webpage metadata](https://developer.mozilla.org/en-US/docs/Learn%5Fweb%5Fdevelopment/Core/Structuring%5Fcontent/Webpage%5Fmetadata?ref=angularspace.com) is a super helpful starting point. Here’s a quick example: ```typescript export const routeMeta: RouteMeta = { meta: [{ name: 'author', content: 'Eduard Krivanek', },{ property: 'og:title', content: 'Anime Search', },{ property: 'og:description', content: 'Example App about anime search', }]}; @Component({ template: `` }) export default class HomeComponent {} ``` ### Rendering Strategy By default, every page in Analog uses SSR to generate content. But [Analog also supports SSG](https://analogjs.org/docs/features/server/static-site-generation?ref=angularspace.com), and you can even opt out completely and go with CSR. The [SSR vs CSR article](https://www.freecodecamp.org/news/server-side-rendering-javascript/?ref=angularspace.com) is a solid read if you are wondering when to use what. In our case, we use SSR for the `/` route, the homepage, since it fetches anime data right away and benefits from that initial server render. The `/my-list` page, however, is behind an auth wall, so SEO isn't really a concern. That raises the question: “Should I bother with SSR here?” Personally, I’d say no. For dashboard-style pages, CSR usually does the job just fine. Then we’ve got `/login`, `/register`, and all the blog pages. These don’t change often, so they’re perfect candidates for SSG. To achieve this configuration, you’ll need to adjust the `plugins` section in your `vite.config.ts`. ```typescript plugins: [ analog({ prerender: { routes: async () => [ '/login', '/register', { contentDir: 'src/content/blog', transform: file => { const slug = file.attributes?.['slug'] || file.name; return `/blog/${slug}`; }, }, ], }, nitro: { routeRules: { '/my-list': { ssr: false }, }, }, }), ], ``` Worth mentioning that even if you leave some pages to be rendered by SSR, you can still use `@defer (on viewport){}` to load child components, such as charts, or mainly components which work with DOM, only the client side. For that read more about [defer syntax from Angular docs](https://angular.dev/guide/templates/defer?ref=angularspace.com). ## Markdown Content Generation One feature I really appreciate as a blog-posting human is [markdown content support](https://analogjs.org/docs/features/routing/content?ref=angularspace.com) in Analog. For our app, we want to display three blog posts, which we’ll store in `/src/content/blog`. We want to set up routing, render the posts, add formatting, and even dynamic route metadata. First step it to open `src/app/app.config.ts`, enable markdown file support, and add a syntax highlighter. You can pick between [Prism](https://prismjs.com/?ref=angularspace.com) or [Shiki](https://shiki.matsu.io/?ref=angularspace.com), both are great, but for this example, we’re rolling with Prism. ```typescript import { ApplicationConfig } from '@angular/core'; import { provideContent, withMarkdownRenderer } from '@analogjs/content'; import { withPrismHighlighter } from '@analogjs/content/prism-highlighter'; export const appConfig: ApplicationConfig = { providers: [ // ... other providers provideContent(withMarkdownRenderer(), withPrismHighlighter()), ], }; ``` When it comes to markdown files (in the `/src/content/blog` folder), it’s way easier to keep all your blog posts in a single folder, instead of creating a new folder for each post. Before listing out the blogs, you’ll probably want to add some metadata to each file, like this: ```typescript --- title: 'Test Blog 1' tags: angular, rxjs order: 1 datePublished: 01.01.2024 coverImage: article-cover/background.jpg --- ## Lorem Ipsum "Neque porro quisquam est qui dolorem..." ``` You can access the [content file list](https://analogjs.org/docs/features/routing/content?ref=angularspace.com#using-the-content-files-list) by the `injectContentFiles` function, that takes a filter where you pass in the location of your content, in our case the `/src/content/blog` folder. ```typescript import { injectContentFiles } from '@analogjs/content'; @Component({ imports: [RouterLink], template: ` @for (post of posts; track post.attributes.title) {
Blogpost - {{ post.attributes.title }}
} `}) export default class HomeComponent { readonly posts = injectContentFiles<{ title: string; tags: string; datePublished: string; coverImage: string; }>(files => files.filename.includes('/src/content/blog')); } ``` The `post.slug` is simply the filename of your blog post. So if a user clicks on `test-blog-1.md`, they’ll be taken to `/blog/test-blog-1`, and the `blog/[slug].page.ts` route will catch and render it. To display the content of the blogpost, inside the `blog/[slug].page.ts` route, analog provides the `injectContent` function and a `MarkdownComponent` to display the blogpost in a formatted way. ```typescript @Component({ imports: [AsyncPipe, MarkdownComponent], template: ` @if (post$ | async; as post) {
} `, }) export default class BlogPostComponent { readonly post$ = injectContent<{ title: string; tags: string; datePublished: string; coverImage: string; }>({ param: 'slug', subdirectory: 'blog', }); } ``` At this point the markdown may be rendered, but styles will not be applied. To fix that, you need to import `prismjs` styles. If you’re using Tailwind, you’ll also want to add [tailwindcss-typography](https://github.com/tailwindlabs/tailwindcss-typography?ref=angularspace.com) for that extra polish (and apply the `prose` css class where you rendered the blogs). Depending on your Tailwind version (mine’s 4+), you’ll need to edit your `style.css`. ```css /* Tailwind directives */ @import 'tailwindcss'; @import 'prismjs/plugins/toolbar/prism-toolbar.css'; /* check node_modules/prismjs/themes/ for the available themes */ @import 'prismjs/themes/prism-tomorrow'; /* highlight text for markdown */ @plugin "@tailwindcss/typography"; ``` Lastly, if you’re heavy on code snippets in your markdown and notice some languages (like SQL, GraphQL, etc.) aren't styled properly, you’ll need to update the `vite.config.ts` file. Just add them under `content.prismOptions.additionalLangs`, like so: ```typescript // vite.config.ts export default defineConfig(({ mode }) => ({ plugins: [ analog({ content: { prismOptions: { additionalLangs: ['yaml', 'sql', 'graphql', 'bash'], }, }, prerender: { /* ... */ }, nitro: { { /* ... */ }, }), ], ``` ### Markdown Content Meta Tags The `blog/[slug].page.ts` is a generic component that handles rendering every markdown file living in `/src/content/blog`. But we also want to add some dynamic meta tags when we prerender these pages for better SEO. If you want a deeper dive, I recommend checking out [Brandon’s example on GitHub](https://github.com/analogjs/analog/blob/beta/apps/blog-app/src/app/pages/blog/resolvers.ts?ref=angularspace.com). For our case though, we can keep it simple with the following setup: ```typescript // src/app/pages/blog/[slug].page.ts export const routeMeta: RouteMeta = { title: 'Blog Post', meta: route => { const file = injectContentFiles<{ title: string; tags: string; datePublished: string; coverImage: string; }>().find(file => file.slug === route.params['slug'])!; return [{ name: 'author', content: 'Eduard Krivanek', },{ property: 'og:title', content: file.attributes.title, }, { property: 'og:published', content: file.attributes.datePublished, }]; }, }; @Component({ template: `...`}) export default class BlogPostComponent { /* ... */ } ``` This configuration will automatically inject the correct meta tags into each prerendered blog page. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/full-stack-analog-application-dynamic-metatags.png) Dynamic Metatags On Blogs ## API routes Each app needs a database connection to persist data. In our demo app I mocked this database, but the idea remains the same. We’ve got one singleton service (class) that stores all the application data. ```typescript export class Database { static instance = new Database(); storedData = {}; async createUser(username: string) { /* .. */ } async addLikedAnime(username: string, anime: AnimeDetails) { /* .. */ } async removeLikedAnime(username: string, animeId: number) { /* .. */ } } ``` You’ll access every method via `Database.instance.XYZ`. Now, what we want to do is create an endpoint based on the [HTTP request method](https://analogjs.org/docs/features/api/overview?ref=angularspace.com#specific-http-request-method). This endpoint will allow us to pass information, like when a user marks an anime as a favorite and wants to keep it in their "favourites" section, then even display it on the `(anime)/my-list` page. To create this endpoint, we’ll use the `defineEventHandler` function. This function will be placed in `src/server/routes/api/anime/save-anime.post.ts` with the following logic. One thing to note is the `.post` suffix in `save-anime.post.ts`. This suffix tells Analog to create a specific HTTP POST endpoint. If you removed the `.post` and kept the file as `save-anime.ts`, this endpoint would accept any HTTP method. In our case, we want to restrict it to just POST requests. ```typescript // src/server/routes/api/anime/save-anime.post.ts import { createError, defineEventHandler, readBody } from 'h3'; import { AnimeDetails } from './../../../api/api.model'; import { Database } from './../../../database/database'; export default defineEventHandler(async event => { const body = await readBody(event) const id = parseInt(body?.id); const username = body?.username; // check if id is a number if (!Number.isInteger(id) || !username) { throw createError({ statusCode: 400, statusMessage: 'Invalid input data', }); } // fetch anime details from API const response = await fetch(`https://api.jikan.moe/v4/anime/${id}`); const anime = (await response.json()) as { data: AnimeDetails }; // save anime to DB await Database.instance.addLikedAnime(username, anime.data); // return liked anime to the user return { data: anime.data }; }); ``` For POST requests, the `readBody` function will let you access the message body. If you were using a GET request, you’d use `getRouterParam` instead. To send data to this backend listener, you’ll need to use `HttpClient` and target the `/api/anime/save-anime` URL. ```typescript @Injectable({ providedIn: 'root' }) export class AnimeApiService { private readonly http = inject(HttpClient); saveAnime(id: number | string, username: string) { return this.http.post('/api/anime/save-anime', { id, username, }); } } ``` ## API Route Protection Analog supports [server-side middleware](https://analogjs.org/docs/features/routing/middleware?ref=angularspace.com), designed to modify requests, sending redirects or checking for authentication. In our example we can use middleware to setup route protection on every `/api/anime` route, verifying if user is authenticated. Each middleware run in order defined in the `/server/middleware` folder. ```typescript // /server/middleware/is-logged-in.ts import { defineEventHandler, getHeader, getRequestURL, sendRedirect } from 'h3'; export default defineEventHandler(async event => { if (getRequestURL(event).pathname.includes('/api/anime')) { const authToken = getHeader(event, 'authToken'); // check auth and redirect if (!authToken) { console.log('User not logged in, redirecting to login'); sendRedirect(event, '/login', 401); } } }); ``` You either add a header to each request or create an interceptor for that, however right now if you target the `/api/anime` endpoint without the `authToken` information, you will get the following error message. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/full-stack-analog-application-error.png) Middleware No Token ## Form Actions Analog comes with [form actions](https://analogjs.org/docs/guides/forms?ref=angularspace.com), which allow you to handle form submissions directly, without needing a separate HTTP route to process the data (as we did earlier). Instead, the logic is placed right next to the file where the form is submitted. The `FormAction` directive, imported from `@analogjs/router`, gives you access to `onSuccess` and `onError` event emitters on the `
` tag. To demonstrate this, we’ll use the `(auth)/login.page.ts` to handle user sign-in. The client-side component looks like this: ```typescript @Component({ imports: [FormAction, /* .... */], template: ` Username
`, }) export default class LoginComponent { readonly form = new FormGroup({ username: new FormControl('testname', { validators: [Validators.required], }), }); onSuccess(data: unknown) { console.log('success', data as AuthUser); } onError(result?: unknown) { console.log('error', result); } } ``` To handle form submissions, you create a `.server.ts` alongside the `.page.ts` component. So, in our case, the folder structure looks like this: ```markdown src/ └── app/ └── pages/ ├── (auth)/ │ ├── login.page.ts │ └── login.server.ts │ └── register.page.ts │ └── register.server.ts ``` And inside the `login.server.ts` you may have a logic similar to this ```typescript import { fail, json, type PageServerAction } from '@analogjs/router/server/actions'; import { readFormData } from 'h3'; import { Database } from '../../../server/database/database'; export async function action({ event }: PageServerAction) { const body = await readFormData(event); const username = body.get('username') as string; if (!username) { return fail(422, { username: 'Username is required' }); } if (!(await Database.instance.getUser(username))) { return fail(422, { username: 'Invalid username' }); } return json({ type: 'success', token: 'XYZ', username: username }); } ``` An interesting thing I noticed is that `onSuccess` fires when the user authenticates successfully, but the response is always returned as an `unknown` type. So, if you try to write `onSuccess(data: AuthUser) {}`, you’ll get an error: `Argument of type 'unknown' is not assignable to parameter of type 'AuthUser'`. You could validate the data using Zod, or as shown in the example, just cast it to the right type manually. In the `LoginComponent`, we used `FormGroup` to build the form. However, with form actions, this isn't strictly necessary. You just need to display the form fields in the UI, assign a unique `name` to each one, and when the form is submitted, it will send the data using `method="post"` to the form handler. The `FormGroup` was mainly useful for adding validators in this case. ## Summary Overall, my experience with Analog has been a pleasant one. The documentation is well-organized and provides all the necessary information. And if something’s unclear, you can always hop onto their Discord to ask questions or chat with the community. But the real question is: would I choose Analog for my next project? Right now, it’s tough to imagine the Angular team coming up with a full-stack solution that could outshine Analog. More and more developers are aware of Analog, so I don't think it's going anywhere anytime soon. If you’re not working on a fully dashboard-style app (where most of the functionality is behind authentication and you don’t want to set up a separate backend like NestJS), Analog is definitely worth considering. It shines in small to medium-sized projects. For large-scale projects, though? Check with the community on the [Discord channel](https://discord.com/invite/mKC2Ec48U5?ref=angularspace.com). I hope you enjoyed this article. You can check out the full Anime search example on [GitHub](https://github.com/krivanek06/analogjs-example-project?ref=angularspace.com). Feel free to share your thoughts, catch more of my articles on [dev.to](https://dev.to/krivanek06?ref=angularspace.com), or connect with me on [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/Screenshot-2024-11-13-at-15.52.00--1---6---1---4-.jpg) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/05/angular-university-banner-4--2-.jpg)](https://angular-university.io/?ref=angularspace.com) [](https://ghost.org/help/using-the-editor/?ref=angularspace.com) --- ### Angular Animation Magic: Unlock the Power of the View Transition API URL: https://www.angularspace.com/angular-animation-magic-unlock-the-power-of-the-view-transition-api/ Last updated: 2025-05-01T20:22:03.000Z ## 🪄 Introduction: A Glimpse of Magic When I first saw a presentation about the View Transition API, it blew my mind. It is truly magical! The core idea is that the browser captures a visual snapshot of DOM elements marked with the `view-transition-name` CSS property before a DOM change. After the DOM change, the differences are animated using CSS Animations. To truly appreciate its capabilities, take a look at these examples: - [Example 1: View Transitions like IsotopeJS](https://codepen.io/argyleink/full/VwBKjwj?ref=angularspace.com) - [Example 2: Adding and removing cards](https://view-transitions.chrome.dev/cards/spa/?ref=angularspace.com) - [Example 3: Playlist app](https://live-transitions.pages.dev/?ref=angularspace.com) Here's a classic code example illustrating how to trigger a view transition: ```js function handleClick(e) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { updateTheDOMSomehow(); return; } // With a View Transition: document.startViewTransition(() => updateTheDOMSomehow()); } ``` `updateTheDOMSomehow`? Somehow?? And it will just work? Yep, it will. This highlights the remarkable flexibility of the API. I won't delve into the fundamentals of the View Transition API in this article. The [official documentation](https://developer.chrome.com/docs/web-platform/view-transitions?ref=angularspace.com) provides excellent information if you're not already familiar with it. A basic understanding of Angular components, signals, and CSS is recommended. ## 🚧 The Angular Challenge: Beyond Route Transitions Working with the View Transition API in Angular is already possible for navigating between routes. However, animating individual elements on a page [is not supported out of the box](https://github.com/angular/angular/issues/55829?ref=angularspace.com). How can this gap be bridged? ### ⚙️ Setting the Stage Begin by creating a basic Angular component to experiment with: ```ts @Component({ selector: 'basic-demo', styleUrl: './basic-demo.component.scss', template: `
{{ position() }}
`, }) export class ViewTransitionBasicDemoComponent { position = signal<'left' | 'right'>('left'); toggle() { this.position.set(this.position() === 'left' ? 'right' : 'left'); } } ``` This component displays a box initially positioned on the left. Clicking the button toggles its visual position between the left and the right, while also displaying the current position within the box. The CSS has been omitted for brevity. Find the complete example on [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-wpq3wtdj?file=src%2Fmain.ts&ref=angularspace.com). ![State 1 video](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/state-1-video.gif) Now, attempt to animate the element using the View Transition API. First, add the following to the application's global styles: ```css :root { view-transition-name: none; } ``` This ensures that only elements explicitly marked with `view-transition-name` will be animated. According to the View Transition API, two key elements are needed for the animation to work: 1. The element intended for animation must have the `view-transition-name` CSS property set with a unique value. 2. The `document.startViewTransition()` function needs to be called. The provided callback function should update the DOM. Applying the CSS property is straightforward: ```html
{{ position() }}
``` However, the second part presents a challenge in Angular. A naive attempt might look like this: ```ts toggle() { document.startViewTransition(() => { this.position.set(this.position() === 'left' ? 'right' : 'left'); }); } ``` ... and surprisingly, it works! The animation occurs. See it in action on [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-4rdswmgu?file=src%2Fmain.ts&ref=angularspace.com). ![State 2 video](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/state-2-video.gif) You will see why this is rather surprising as we introduce a little bit more complexity to our example and talk more about SPA paradigm on rendering. Let's add a `computed` property and another DOM element that would be shown depending on the new `computed` property: ```ts @Component({ selector: 'app-root', styles: `// omitted for brevity`, template: `
{{ position() }}
@if (isCircleVisible()) {
} `, }) export class App { position = signal<'left' | 'right'>('left'); isCircleVisible = computed(() => this.position() === 'right'); toggle() { document.startViewTransition(() => { this.position.set(this.position() === 'left' ? 'right' : 'left'); }); } } ``` Now, while toggling between "left" and "right", the *box* still animates, but the *circle* is not animated despite having `view-transition-name` CSS property as seen in this [StackBlitz example](https://stackblitz.com/edit/stackblitz-starters-q8ee6g1l?file=src%2Fmain.ts&ref=angularspace.com). ![State 3 video](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/state-3-video.gif) ### 🔄 The SPA Paradigm: Data Updates vs. DOM Manipulation Recall that the `startViewTransition` function's callback expects a function that updates the DOM: ```js document.startViewTransition(() => { updateTheDOMSomehow(); }); ``` However, in Single Page Applications (SPAs) like Angular, developers typically work with data (state) and rely on the framework to render the DOM in response to data changes. Direct DOM manipulation is generally avoided. So, the `updateTheDOMSomehow()` part isn't directly available to a developer working with Angular. Instead, the developer would write the `updateTheDataSomehow()` function, which doesn't directly align with the View Transition API's requirement. The animation works for the *box* in the simpler case likely because Angular is fast enough to synchronize the state change and the DOM update within the execution of the callback function: ```ts document.startViewTransition(() => { this.position.set(this.position() === 'left' ? 'right' : 'left'); // Angular is quick enough to update DOM dependent on `position` here }); ``` However, the `computed` property likely schedules a separate render tick that occurs outside the `startViewTransition` callback. This explains why the *circle* isn't animated. Furthermore, the *box* animation might also become inconsistent. The animation might work when switching from left to right but fail from right to left. Removing the *circle*\-related code might resolve this inconsistency. The key takeaway is that Angular renders DOM changes at its own pace, influenced by various factors. The data can't simply be updated within the `startViewTransition` callback with the expectation that the DOM would be ready immediately. There needs to be a way to ensure the DOM has been updated within that callback: ```ts document.startViewTransition(async () => { this.position.set(this.position() === 'left' ? 'right' : 'left'); await angularUpdatedTheDOM(); // <-- Need to do this }); ``` ### 🌉 Bridging the Gap between the View Transition API and Angular To adhere to the Single Responsibility Principle, encapsulate the view-transition-related logic within a dedicated service: ```ts @Injectable({ providedIn: 'root' }) export class ViewTransitionService { run(stateChangeFn: () => void) { if (!document.startViewTransition) { stateChangeFn(); return; } document.startViewTransition(async () => { stateChangeFn(); await createRenderPromise(); // <-- TODO }); } } ``` Angular provides the `afterNextRender` function, which is precisely what is needed to create the `createRenderPromise` function: ```ts function createRenderPromise(injector: Injector) { return new Promise((resolve) => { afterNextRender({ read: () => { resolve(); } }, { injector }); }); } ``` Now, update the component to utilize this new service: ```ts export class App { private viewTransitionService = inject(ViewTransitionService); position = signal<'left' | 'right'>('left'); isCircleVisible = computed(() => this.position() === 'right'); toggle() { this.viewTransitionService.run(() => { this.position.set(this.position() === 'left' ? 'right' : 'left'); }); } } ``` With this service in place, all animations should now work as expected. See it running on [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-hamxeyrx?file=src%2Fmain.ts&ref=angularspace.com). ![State 4 video](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/state-4-video.gif) ### 🚦 Handling Concurrent Transitions: A Potential Pitfall Let's refine our demo slightly. First, slow down all view transition animations to 3 seconds to observe the behavior more clearly: ```scss ::view-transition-group(*) { animation-duration: 3s; } ``` Now, add another element - a *triangle* \- that will be animated somewhat independently: ```html @if (isTriangleVisible()) {
} ``` For simplicity, update the component to show the *triangle* after a short delay: ```ts isTriangleVisible = signal(false); toggle() { this.viewTransitionService.run(() => { this.position.set(this.position() === 'left' ? 'right' : 'left'); }); setTimeout(() => { this.viewTransitionService.run(() => { this.isTriangleVisible.set(!this.isTriangleVisible()); }); }, 700); } ``` See this implementation on [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-sn6zps7m?file=src%2Fmain.ts&ref=angularspace.com). Also add some logging to the `run` method of the `ViewTransitionService`: ```ts run(stateChangeFn: () => void) { // ... console.log('start transition'); document.startViewTransition(async () => { console.log('state change'); stateChangeFn(); await createRenderPromise(this.injector); console.log('rendered'); }); } ``` Upon pressing the "toggle" button, an unexpected behavior is observed. The first two elements begin animating, but after 700ms, the animation abruptly stops. The animated elements jump to their final state and then the *triangle* element starts its animation. ![State 5 video](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/state-5-video.gif) The console output shows: ``` start transition state change rendered start transition state change rendered ``` This behavior is actually by design. Currently, only one view-transition animation can run at any given time. If a new view-transition is initiated while another is in progress, the ongoing transition is canceled. While there's a proposal for [Scoped View Transitions](https://github.com/WICG/view-transitions/blob/main/scoped-transitions.md?ref=angularspace.com), it's not yet available. From the service's perspective, adding configuration options to handle this could be considered. For instance, the `run` method could accept an options argument to define how an incoming animation should behave: (1) cancel the previous animation, (2) be skipped, or (3) wait for the previous animation to complete. While exploring these implementations could be valuable, there's another crucial scenario that needs to be discussed. ### ⏱️ A Case For Optimizing the Overlapping Transitions Let's reduce the timeout for starting the *triangle* animation from `700ms` to `2ms`. Now, clicking the "Toggle" button will likely still result in the second animation canceling the first. However, a different console output might be seen: ``` start transition start transition state change rendered state change rendered ``` The `createRenderPromise()` method takes a small amount of time to complete, potentially slightly more than 2ms on some systems. This means that both view transitions are being started before Angular has a chance to render the changes from the first state update. This presents an opportunity for optimization. Below is a visualization of the timing of view transitions within the Angular rendering cycle: ![Anatomy of View Transition](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/vt-anatomy.png) 1. **Initial Snapshot (Yellow):** This occurs when `document.startViewTransition` is called. It likely involves capturing the initial snapshot of the relevant DOM elements. 2. **State Change and Render (Blue):** The view transition callback is executed. Here, the Angular state is updated, which triggers DOM changes. Then the render promise waits for Angular to reflect these changes in the DOM. 3. **Final Snapshot (Yellow):** Once the callback completes, the browser takes the final snapshot of the DOM. 4. **Animation (Red):** Finally, the browser performs the view transition animation. In the initial scenario with a 700ms delay, the timeline of the two view transitions looked like this: ![Timing with 700ms delay](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/700ms.png) However, with the 2ms delay, the timeline looks like this: ![Timing with 2ms delay](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/2ms.png) As seen above, there is an overlap in the blue blocks where state changes are applied. In this case, a second view transition doesn't need to be initiated. Instead, the second state change should ideally be applied as part of the first view transition since Angular hasn't finished rendering yet: ![Desired result](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/desired.png) This might seem like a minor edge case, but it's a critical aspect of effectively handling view transitions in Angular. To address this, modify the `run` method in the `ViewTransitionService` to: 1. Buffer each incoming `stateChangeFn` without immediately executing it. 2. If no view transition is currently active, start one. Within the view transition callback, execute all the buffered `stateChangeFn` in order. 3. If a view transition is already in progress: - if Angular did not complete rendering cycle yet, execute the buffered `stateChangeFn` functions - if Angular completed the rendering cycle and View Transition API started the animation, schedule another view transition `.run` call once the current view transition completes Use a buffer to ensure that the state change functions aren't lost in case they come after Angular has completed the rendering cycle for the current view transition. This also ensures that state change functions are executed in the order they were received (FIFO). Find a simplified version of the `ViewTransitionService` with these changes applied on [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-mn6y4v1i?file=src%2Fview-transition.service.ts&ref=angularspace.com). The code for this is becoming quite extensive, so for the remaining examples, I'll primarily provide references to StackBlitz for longer code snippets. With this updated service, pressing the "Toggle" button should now execute all animations smoothly: ![State 6 video](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/state-6-video.gif) ### 🛠️ Refining the API: Moving Closer to the DOM While the `ViewTransitionService` is functional, the developer experience can be further improved. Currently, every time something needs to be animated, the corresponding state change needs to be wrapped within `viewTransitionService.run(() => { /* ... */ })`. This can lead to scattering rendering logic throughout the application's business logic, making it less maintainable. Ideally, there should be a way to mark elements for view transitions directly within the DOM structure. Something like this: ```html
Element to animate - {{stateProp}}
``` There needs to be a directive that observes changes to the provided `stateProp`. Right before the state changes, call `viewTransitionService.run(() => { /* ... */ })` to allow the View Transition API to capture the "start" snapshot. The callback of the run method would then contain the actual state update. ### 🤔 The Challenge of Timing: Capturing the "Before" State This raises the question of how to intercept the moment precisely before the state change is reflected in the DOM. Angular's `ngOnChanges` lifecycle hook is executed after the property has already changed, making it too late to reliably capture the initial state for the view transition. [Younes Jaaidi](https://x.com/yjaaidi?ref=angularspace.com) has conducted [remarkable experiments](https://marmicode.io/blog/angular-signals-and-custom-render-strategies?ref=angularspace.com) exploring ways to intervene between the state change and the DOM update in Angular. However, his solution involves monkey-patching private Angular APIs. While his approach would provide a cleaner directive API - our API will opt for a less experimental approach. The directive will rely on an extra piece of state that will be responsible for modifying the DOM after `viewTransitionService.run` is invoked. ### ✨ Introducing the \*vt Directive: A Custom Rendering Strategy Here's a basic implementation of such a directive: ```ts @Directive({ selector: '[vt]', standalone: true, }) export class ViewTransitionRenderer { private viewTransitionService = inject(ViewTransitionService); private document = inject(DOCUMENT); private templateRef = inject(TemplateRef); private viewContainerRef = inject(ViewContainerRef); private cdr = inject(ChangeDetectorRef); trackingData = input.required({ alias: 'vt' }); private context: Context = createContext(null as any); ngOnChanges(changes: Changes) { const shouldAnimate = !changes.trackingData.firstChange; // assign these variables outside of the callback to avoid closure issues const firstChange = changes.trackingData.firstChange; const currentValue = changes.trackingData.currentValue; if (!this.document.startViewTransition || !shouldAnimate) { this.render(firstChange, currentValue); return; } this.viewTransitionService.run(() => { this.render(firstChange, currentValue); }); } private render(isFirstChange: boolean, trackingData: T) { this.context.$implicit = trackingData; if (isFirstChange) { this.viewContainerRef.createEmbeddedView(this.templateRef, this.context); } this.cdr.detectChanges(); } } interface Context { $implicit: T; } function createContext(data: T): Context { return { $implicit: data }; } ``` In this directive, the `trackingData` input holds the value that will be rendered when the view transition is ready to proceed – specifically, within the callback of the `viewTransitionService.run` method. Inside the `.run` callback, the input's value is assigned to the `$implicit` context of the directive. The expectation is that the template within the `*vt` directive will now use the directive's implicit context variable instead of the original state property passed to the directive. ```html
Element to animate - {{state}}
``` Notice the usage of `{{state}}` instead of `{{stateProp}}` in the template. A slightly more convenient syntax can be used by re-assigning the same name as the original property to the `$implicit` context variable: ```html
Element to animate - {{stateProp}}
``` This approach works, although its suitability as a best practice might be debatable. Thoughts? With this directive in place, the demo component can be updated to leverage the directive: ```ts @Component({ selector: 'app-root', imports: [ViewTransitionRenderer], template: `...`, }) export class App { position = signal<'left' | 'right'>('left'); isCircleVisible = computed(() => this.position() === 'right'); isTriangleVisible = signal(false); toggle() { this.position.set(this.position() === 'left' ? 'right' : 'left'); setTimeout(() => { this.isTriangleVisible.set(!this.isTriangleVisible()); }, 2); } } ``` Template: ```html
{{ position }}
@if (isCircleVisible) {
}
@if (isTriangleVisible) {
}
``` See this fully implemented on [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-bjrpkm3a?file=src%2Fmain.ts&ref=angularspace.com). ![State 7 video](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/state-6-video.gif) Below is a diagram showing the data flow between the consuming component, the `*vt` directive, the `ViewTransitionService` and the View Transition API: ![Data flow](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/data-flow.png) ### 🎯 Fine-Grained Control: Enabling View Transitions on Demand Let's modify the demo to trigger the animation for the triangle element separately: ```html ``` ```ts toggleBox() { this.position.set(this.position() === 'left' ? 'right' : 'left'); } toggleTriangle() { this.isTriangleVisible.set(!this.isTriangleVisible()); } ``` Now, open `Chrome Dev Tools > Animations`, click the "Pause" button, and then click "Toggle Triangle." Navigate to the "Elements" tab and inspect the active view transitions. See something like this: ```html ::view-transition ::view-transition-group(box) ::view-transition-group(circle) ::view-transition-group(triangle) ``` This indicates that even though only the *triangle* is intended to be animated, the *box* and *circle* are also part of the view transition. While their state might remain unchanged, this can have several drawbacks: - The timing of other animations could influence the view transition timing of the intended element. - Having numerous active animation simultaneously might lead to interference between them. - Debugging animations becomes more complex when all of them are active by default. A mechanism is needed to selectively enable or disable view transitions based on the intent. This mechanism should be automated as much as possible. In the current demo, all three elements are always included in the view transition because they always have a `view-transition-name` CSS property set. To disable a view transition on an element, this property needs to be set to `none`. Create another directive that works in conjunction with the `*vt` directive. This directive will allow the desired `view-transition-name` value to be specified only when an animation is active: ```html @if (isTriangleVisible) {
}
``` By default, this directive will apply the value `none` to the `view-transition-name` style. However, when the parent `*vt` directive initiates a view transition, the `vtName` directive will apply the specified value. Once the view transition completes, it will revert back to `none`. I won't include the full implementation code for this directive here to keep the article concise. If interested in the implementation details - find them on [Github](https://github.com/DmitryEfimenko/ngspot/tree/main/packages/view-transition/package?ref=angularspace.com). ### 🧩 Advanced Use Cases: Animating an Item in the Lists and Customizing Transitions Before concluding, consider another common scenario. Imagine a list of cards displayed in a column, driven by a `for` loop. Each card has "up" and "down" buttons to move it within the list: ```ts @Component({ // ... }) export class Cards { cards = signal([0, 1, 2, 3]); up(id: number)) { this.cards.update(move(id, 'up')); } down(id: number)) { this.elements.update(move(id, 'down')); } } ``` ```html @for (card of cards(); track card) {
} ``` To achieve a basic animation of card movement, wrap the container in the `*vt` directive and apply the `[vtName]` directive to each card: ```html @for (cardId of cards; track cardId) {
}
``` Running this code will produce the expected animation: ![reordering cards video](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/reordering-cards-video-1.gif) However, what if there is a requirement to customize the animation of the specific card that was clicked? That element's view transition can not be easily targeted because all cards have dynamically generated `view-transition-name` values: ```css ::view-transition-image-pair(???) { animation: size-up-and-down ease-in 0.5s; } ``` There needs to be a way to dynamically set a custom `view-transition-name` on the clicked element. A new property and a method could be introduced in the `ViewTransitionService`: ```ts activeViewTransitionNames = signal(null); setActiveViewTransitionNames(...ids: string[]) { this.activeViewTransitionNames.set(ids); } ``` This method would be expected to be called right before the view transition starts. Once the transition finishes, the `activeViewTransitionNames` would be reset: ```ts this.currentViewTransition.finished.finally(() => { this.activeViewTransitionNames.set(null); }); ``` With this in place, a new directive: \[vtNameForActive\] can be introduced. This directive would check if the `activeViewTransitionNames` signal contains the name provided to the `[vtName]` directive on the same element. If it does, it would apply a specific view-transition-name (e.g., 'target-card'): ```html
``` The Cards component would then be updated as follows: ```ts export class Cards { private viewTransitionService = inject(ViewTransitionService); cards = signal([0, 1, 2, 3]); up(id: number)) { this.viewTransitionService.setActiveViewTransitionNames(`card-${card.id}`); this.cards.update(move(id, 'up')); } down(id: number)) { this.viewTransitionService.setActiveViewTransitionNames(`card-${card.id}`); this.elements.update(move(id, 'down')); } } ``` Now, target the clicked element with a specific CSS rule: ```css ::view-transition-image-pair(target-card) { animation: size-up-and-down ease-in 0.5s; } ``` ![reordering cards video 2](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/reordering-cards-video-2.gif) Again, the implementation details of the `[vtNameForActive]` directive are omitted for brevity. Find its implementation within the [@ngspot/view-transition](https://github.com/DmitryEfimenko/ngspot/tree/main/packages/view-transition/package?ref=angularspace.com) package on Github. The full code for making this cards animation can be [found here](https://github.com/DmitryEfimenko/ngspot/tree/main/packages/view-transition/demo/src/lib/view-transition-demo/ordering?ref=angularspace.com). ## 📦 Further Abstraction: The @ngspot/view-transition Package ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/logo-2.png) In this article there was a deep dive into challenges of integrating View Transition API with Angular. All the services and directives mentioned here can be found in the [@ngspot/view-transition](https://github.com/DmitryEfimenko/ngspot/tree/main/packages/view-transition/package?ref=angularspace.com) Package. This package provides several more convenient directives to simplify handling various edge cases. Two notable directives are `[vtNameForRouting]` and `[vtNameForRouterLink]`, which streamline the use of view transitions during route navigation. Explore the [various demos](https://dmitryefimenko.github.io/ngspot/view-transition/isotope?ref=angularspace.com) showcasing the animations made possible in Angular through the View Transition API and the `@ngspot/view-transition` package. As a final thought, I encourage the amazing Angular community to try out this package and provide feedback. I'm confident that there are still many undiscovered challenges and edge cases when integrating the View Transition API with Angular. By collaborating on the API and the tools within the `@ngspot/view-transition` package, we can collectively simplify this powerful integration! 🙌 --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/Screenshot-2024-11-13-at-15.52.00--1---6---1---3-.jpg) ### Breaking the Enum Habit: Why TypeScript Developers Need a New Approach URL: https://www.angularspace.com/breaking-the-enum-habit-why-typescript-developers-need-a-new-approach/ Last updated: 2025-04-29T11:42:36.000Z There is a lot of talk within the Angular and TypeScript community as a whole about the usage of enums. It seems to be a hill both proponents and opponents are willing to die on and defend harshly. Enums are a commonly used TypeScript feature that offers tremendous value and improved developer experience within our codebases, so we don't want to abandon them without good reason and alternatives. Let's dive into the reasons why we might need a new approach to handle enums and what these alternative approaches are. At the end of this article, you can make up your own mind and decide once and for all if you will continue using enums or switch to an alternative approach. ## Why did TypeScript add Enums? Enums were introduced in TypeScript 0.9 (circa 2013) to give developers a type-safe and readable alternative to plain JavaScript constants and "magic numbers or strings" for defining a grouped set of values. Using numeric and string literals directly in your code is error-prone and hard to refactor. Let's say you have three different statuses you're using within your code: "success", "pending", and "failed". If you're using these statuses as string literals throughout your code, you can easily create a typo, resulting in a bug. Also, when you want to change one of these values at some point in the future, finding and renaming all instances within your code can become a cumbersome task where it's easy to miss an instance. Creating a constant was better than 'magic values', but lacked the type-safety TypeScript aims to provide, so to address this issue, TypeScript created enums. The early design of TypeScript was heavily influenced by statically typed languages like C# and Java. Since enums are a common construct within these languages, it made sense for TypeScript to adopt enums as a solution for JavaScript constants and "magic numbers or strings". Furthermore, using enums would also appeal to developers who came from strictly typed languages, facilitating better adoption. While the introduction of TypeScript enums was an improvement over the plain JavaScript approaches for sure, the shortcomings of TypeScript enums also became apparent early on. As there is no such thing as enums in JavaScript, and it's just syntactic sugar, they never could live up to the enums we know and love from true statically typed languages. ## Unveiling TypeScript Enums To better understand the shortcomings of typeScript enums, let's start by lifting the veil and explore what happens with TypeScript enums during the compilation process. TypeScript is a compile-time tool, and after performing the type checking, code analysis, and error detection, all TypeScript code is erased during the compilation process. Enums (namespaces, modules with runtime code, parameter properties, and Non-ECMAScript import and export assignments) are an exception to this rule and result in compiled JavaScript code that ends up in your runtime JavaScript bundles. The fact that enums breaks the TypeScript paradigm and results in compiled JavaScript code inside your bundles is actually the first and maybe the largest issue. Before we dive into why this is an issue, let's see how the TypeScript compiler (tsc) compiles enums. Let's take the following `Status` enum as an example: ```typescript enum Status { Success, Pending, Failed, } ``` TypeScript will compile enums to an immediately invoked function expression (IIFE): ```typescript var Status; (function (Status) { Status[(Status["Success"] = 0)] = "Success"; Status[(Status["Pending"] = 1)] = "Pending"; Status[(Status["Failed"] = 2)] = "Failed"; })(Status || (Status = {})); ``` When you run this IIFE, it generates the following object: ```typescript { 0: 'Success', 1: 'Pending', 2: 'Failed', Success: 0, Pending: 1, Failed: 2 } ``` As you can see, for numeric enums the TypeScript compiler creates an object with bi-directional mapping. While this makes debugging and logging easier, it also generates additional code you might not be aware of when defining your enum. If you only have a few enums, this might not be a significant concern, but the extra code can add up considerably when developing large software with hundreds or even thousands of enums. Now let's examine a string-based `Status` enum: ```typescript enum Status { Success = "success", Pending = "pending", Failed = "failed", } ``` Just as before, the TypeScript compiler will compile this code to an IIFE: ```typescript var Status; (function (Status) { Status["Success"] = "success"; Status["Pending"] = "pending"; Status["Failed"] = "failed"; })(Status || (Status = {})); ``` When you run this IIFE, you'll notice the output differs from the numeric enum: ```typescript { Success: 'success', Pending: 'pending', Failed: 'failed' } ``` A bi-directional object was created for the numeric enum, whereas a unidirectional object was created for the string enum. While this may seem like a slight discrepancy, there are many nuanced differences in TypeScript enums. These nuances can lead to inconsistent usage and potential bugs when developers aren't fully aware of these subtleties. To throw another alternative into the mix, besides numeric and string-based enums, there are also `const` enums. Before we start to dive deeper into the potential issues of using enums and the proposed alternative solutions, let's see what `const` enums are and how they are handled during the compilation process. You define a `const` enum by simply prefixing it with `const`. Below, we can see the Status enum we used before as a `const` enum: ```typescript const enum Status { Success = "success", Pending = "pending", Failed = "failed", } ``` Contrary to regular TypeScript enums, `const` enums are completely erased at compile-time, only leaving the values you use within your code, so if you define the following value within your code `const status = Status.Success;`, the tsc compiles it into a simple JavaScript variable (`var status = 0; /* Success */`) and removes the enum object itself. As with most things in software, there are trade-offs between regular and `const` enums, and each has its place if you insist on using TypeScript enums. Now that we unveiled TypeScript enums and you should better understand how they work behind the scenes, let's dive into the potential issues of enum usages in TypeScript. ## So what exactly is the problem with TypeScript Enums? To make up your mind on whether you should or shouldn't use TypeScript enums, you need a good understanding of the potential issues that are associated with them. What are the arguments opponents of enums have, and are they valid concerns, or is it something you can ignore? In this section, we will go over the most common arguments against using TypeScript enums and provide you with the information you need to make up your mind on the matter. ### Enums are non ereasable TypeScript code As seen in the previous section, TypeScript enums are non-erasable code and will be compiled into JavaScript code, which ends up in your application bundles. For most opponents of TypeScript enums, this is the main issue they have with them. Let's examine if this actually is a problem or if it's overblown a bit. The problem of non-erasable code can be divided into multiple small issues. 1. It goes against the core concept of TypeScript, which is a compile-time library that has no overhead on your production code. The fact that enums aren't erased can be a valid concern in some cases, but more so, it's a matter of principle for most developers. 2. It increases your application bundles. This might not be an issue for small applications, but if you have a large enterprise environment with hundreds or even thousands of enums, it can start to add up. More so when using numeric enums. For every approximate 50 enums value, you will add 1 Kb to your JS bundle, resulting in about \~1-2 milliseconds in additional load time in the browser. On slow (3G) connections, this can run up to \~5 ms. If you have thousands of enums with many values, you can end up adding quite some additional Kb and ms to your bundles and load times. There will be a small additional impact on performance and memory usage because enums generate an IIFE versus alternatives that use simple JavaScript objects. In many cases, this additional load time and bundle size might not be a huge issue. Still, in general, we want to reduce bundle sizes and load time as much as possible, and slight differences can impact conversions and user retention quite a lot! 3. Not all libraries, languages, and tools support non-erasable TypeScript code. This issue has become more relevant recently, most notably because Node.JS added support to run TypeScript files directly, yet this only works for erasable syntax, so enums will not work in this scenario. Other tools like ts-blank-space and Amaro have the same limitation and only with with erasable TypeScript syntax. Because of this, in TypeScript version 5.8 a new [flag](https://www.typescriptlang.org/tsconfig/?ref=angularspace.com#erasableSyntaxOnly) `--erasableSyntaxOnly` was added. When enabling this flag, the tsc will throw an error when you use non-erasable code like enums. I know we are mostly Angular developers here, but using something that doesn't always work can be dangerous because you might not be aware when it does and doesn't work. Now that you know the problems associated with non-erasable TypeScript code, let's look at the next issue: inconsistencies between string and numeric enums. ### String and Numeric Enums aren't the same! We've already seen that string and numeric enums aren't handled the same way during the compilation process. String enums will be compiled into a unidirectional object, whereas a numeric enum will be compiled into a bi-directional object. Because numeric enums are compiled into a bi-directional object, you can use reversed lookups on numeric enums, but this isn't an option on string enums. Below is a reversed lookup on a numeric enum: ```typescript enum Status { Success, Pending, Failed, } const enumKey = Status[Status.Success]; /* Success */ ``` Now, if we try to do the same with a string-based enum, the compiler will throw an error: ```typescript enum Status { Success = "success", Pending = "pending", Failed = "failed", } const enumKey = Status[ Status.Success ]; /* ❌ Property 'success' does not exist on type 'typeof Status' */ ``` To make things more confusing, you can do a reversed lookup on the numeric enum with any number, even if it's not inside the enum: ```typescript enum Status { Success, Pending, Failed, } const status = Status[100]; // This works, but is undefined ``` Besides the difference in the output and reverse lookups, there is also a difference in how you need to define the enums. You may already have noticed that we didn't assign any values for the numeric enum. Numeric enums will assign their values automatically, starting at zero and auto-incrementing by 1 to get the next value. Alternatively, you have the option to assign one or more values. If you assign a number value yourself, and there are non assigned keys after that, TypeScript will start to auto-incremented from the number value you assigned yourself. ```typescript enum Status { Success, // 0 Pending = 3, // 3 Failed, // 4 } enum Direction { Up, // 0 Down = 20, // 20 Left = 3, // 3 Right, // 4 } ``` This seems simple enough, but there is one small gotcha, and that is if you assign a value that has already been auto-generated: ```typescript enum Direction { Up, // 0 Down, // 1 Left, // 2 Right = 1, // 1 Diagonal, // 2 } ``` The above-value assignment fully messes up your enum, resulting in two double-value notations. `Down` and `Right` both have a value of 1, and `Left` and `Diagonal` both have a value of 2\. As you might imagine, this behavior can lead to some unexpected bugs if a developer uses this without being aware of the generated values. Not only did you end up with duplicated values, the reversed lookup on this enum will also not work anymore `Direction[Direction.Left]` will result in `Diagonal` instead of `Left`. This can be seen as its own enum-related issue; there will be no warning or error when you create enums with duplicated values, and for a numeric enum, this will also lead to missing values on the reversed lookup. Contrary to numeric enums, string enums need to be explicitly assigned a value, leaving less room for unexpected behavior, as we demonstrated with the numeric enum. But there is a catch: you can't enforce an enum to be purely string or number-based, meaning you can mix and match string and numeric values in an enum. Because of that, you can assign some values with a string and leave open others: ```typescript enum Direction { Up = 'up', // up Down, // 0 Left // 1 Right // 2 } ``` Again, this might result in unexpected behavior if the developer who implements it isn't fully aware of this. They might expect that the `Down`, `Left`, and `Right` keys will also get a string value assigned because the first value was assigned with a string. If you do want to use enums, don't mix string and numeric values, and I would say prefer string enums or at least always assign explicit values on your enums. You might think we're done with the differences between string and numeric enums, but there is more... Type-checking is also handled differently for string and numeric enums. String-based enums use named typing (also known as nominal typing) and check for type validity on explicit declarations, meaning you need to use the enum and can't directly provide the value even if it's a valid value: ```typescript enum Status { Success = "success", Pending = "pending", Failed = "failed", } function setStatus(status: Status) { // set status } setStatus("success"); // ❌ Argument of type '"success"' is not assignable to parameter of type 'Status'. setStatus(Status.Success); // ✅ This works ``` On the other hand, numeric enums use structural typing (also known as duck typing or shape typing) and type-check based on the structure or shape of an object rather than its explicit identity or name. This means that for a numeric enumn, you can directly pass in the value instead of using the enum: ```typescript enum Status { Success, Pending, Failed, } function setStatus(status: Status) { // set status } setStatus(0); // ✅ This works setStatus(Status.Success); // ✅ This works ``` As you can see, there are many differences between string and numeric enums, and all these nuances can easily lead to issues, especially when working on a large team. Not every developer will know all these nuances, and things can slip through the cracks in reviews. Besides non-erasable code, suppose bundle increases and many differences between numeric and string-based enums, there are other issues related to TypeScript enums, so let's continue to explore. ### You can do a declaration-merge with Enums I would say if you define an enum, you want it to be fully immutable. For the most part, this is also the case, except that you can do a declaration merge with enums. You might be asking what even is a declaration merge. A declaration merge is when you define two objects with the same declaration (name), and the compiler will merge the two objects for you. Below, you can see an example: ```typescript enum Status { Success = "Success", Pending = "Pending", Failed = "Failed", } enum Status { Processing = "processing", } //Alternatively you can also do a declaration merge in other files like this: declare module "./path-to-enum-export" { enum Status { Inactive = "inactive", } } ``` While this may seem useful initially, it can lead to inconsistent usage, unexpected values, and runtime errors. The `declare module` alternative can result in some unexpected behavior. When you extend an enum in a specific file using the `declare module` approach, the added value is never compiled to JavaScript, and it's only a type. As an example, we can take the code below: ```typescript declare module "./path-to-enum" { enum Role { Guest = "guest", SuperAdmin = "superAdmin", } } function logIfUserIsGuest(role: Role) { if (role === Role.Guest) { console.log("Guest user"); } } logIfUserIsGuest(Role.SuperAdmin); ``` In this example, we did a declaration merge on a Role enum and added a Guest and SuperAdmin. Inside the file where we do the declaration merge, we can use these two merged values as if it were a regular enum value. As you can see, inside the logIfUserIsGuest function, we use `Role.Guest`, and inside the function call, we use `Role.SuperAdmin`. The compiler will allow this and will not complain, yet both the `Role.Guest` and the `Role.SuperAdmin` are undefined because they are never compiled into JavaScript code. Our `logIfUserIsGuest(Role.SuperAdmin)` function call will log `Guest user` in this scenario. This should never be done, which begs the question, why is it even allowed? It's true, this should be caught during a code review, but sometimes we don't pay attention that well, especially if it's a large merge, things can slip thought the mazes. There are plenty more issues related to TypeScript enums. At the time of writing, there are [72 open issues](https://github.com/microsoft/TypeScript/issues?q=is%3Aissue+is%3Aopen+enum+label%3Abug&ref=angularspace.com) related to enums on TypeScript GitHub repository. Most of these are minor issues, but it's worth investigating them. If you still want to use TypeScript enums at this point, I recommend using some lint rules to make their usage safer and more consistent. Below are three rules I can recommend: one to enforce explicit value initialization, one to prevent mixing numeric and string enums, and one that prevents duplicate enum values. ```typescript { "rules": { "@typescript-eslint/prefer-enum-initializers": "error", "@typescript-eslint/no-mixed-enums": "error", "@typescript-eslint/no-duplicate-enum-values": "error" } } ``` Now that you better understand some of the issues related to enums let's look at `const` enums, as you might feel they are the better solution. ### Are const Enums any better? At first glance, `const` enums might seem like a better alternative. You can't do a declaration merge on `const` enums, and they don't bloat your bundle sizes as only used values are combined into `const` properties. The code is still compiled to JS; however, even if it's just a simple `const` property, you can't use them with libraries and runtimes requiring only erasable TypeScript code. Also, the differences between numeric and string-based enums remain with `const` enums. Besides that, `const` enums have their own set of issues. Many of the [72 open issues](https://github.com/microsoft/TypeScript/issues?q=is%3Aissue+is%3Aopen+enum+label%3Abug&ref=angularspace.com) on GitHub are related to `const` enums and inside the TypeScript documentation there is a special section dedicated to warning you about [common pitfalls](https://www.typescriptlang.org/docs/handbook/enums.html?ref=angularspace.com#const-enum-pitfalls) when working with `const` enums. Let's explain these common pitfalls in a bit more detail. Using const enum in TypeScript can seem simple at first, but there are some tricky issues you need to watch out for. These problems mostly show up when you share code between projects or publish `.d.ts` files (which happens when you use tsc --declaration to generate type declarations). #### Ambient Const Enums don't work with `isolatedModules`: The `isolatedModules` setting in `tsconfig.json` makes sure that each TypeScript file can be compiled independently. This is often needed if you're using tools like Babel or SWC, which compile `.ts` files one at a time. An ambient enum is one that's defined in a `.d.ts` file—these are declaration files which only describe types and values that are defined somewhere else. For example, this is an ambient const enum in a `.d.ts` file: ```typescript // directions.d.ts declare const enum Direction { Up, Down, Left, Right, } ``` It tells TypeScript what the enum looks like, but it doesn’t actually define the values in any `.js` file. It’s used when a library or module wants to declare what enums exist without including their actual implementation. When using ambient const enums, a problem arises because `const` enums must be inlined, and ambient const enums don't contain the actual values when it's needed. ```typescript // directions.d.ts declare const enum Direction { Up, Down, } // app.ts const dir = Direction.Up; ``` With `isolatedModules: true`, this will error out: ``` "Cannot use 'const enum' with --isolatedModules because values cannot be computed." ``` So if you’re building a library and you publish .d.ts files that include const enums, your users might not be able to use them at all if they have `isolatedModules` turned on. That’s a big compatibility issue. #### Version Mismatches Can Break Code Silently: Because const enum values are inlined at compile time, they get baked into your JavaScript during build. That means if your project compiles using version A of a dependency, the enum values from that version are embedded in your code. But at runtime, if the project loads version B of that same dependency (which might have different enum values), then your code is now using the wrong values. It's possible to compile your code using one version of a dependency and end up running it with another due to how package managers and build systems resolve modules. For example, in a monorepo or multi-package setup, your app might compile against version 1 of a library, inlining its const enum values during the build. However, if another part of the project depends on version 2 of that same library, the package manager (like npm or Yarn) might hoist version 2 to the top level, effectively replacing version 1 at runtime. Similarly, if you're publishing libraries or using shared environments, your code could be built with one version of a dependency but deployed into a system that loads a newer or older one. This mismatch can silently break logic when inlined const enum values no longer match the actual runtime values. This can lead to very confusing bugs, like if statements go down the wrong path because the enum value your code expects doesn't match what it’s actually using at runtime. These bugs are especially tricky because they won’t show up in tests, if your tests and builds are using the same dependency version. They only appear when there’s a mismatch between the compile-time and runtime versions. #### Runtime Errors from Unresolved Imports: If you use `importsNotUsedAsValues: "preserve"` in your `tsconfig.json`, TypeScript keeps imports that are only used for const enum values. That’s fine for regular enums. But with ambient const enums, the actual JavaScript files might not exist at runtime, since `.d.ts` files don’t generate any `.js`. This means your app could crash at runtime because it’s trying to import a file that doesn’t exist. One way to avoid this is to use type-only imports, which are meant to be stripped out by the compiler. But unfortunately, type-only imports can’t be used with `const` enum values right now. So you’re stuck between keeping imports that break at runtime or removing them in a way that doesn’t work with `const` enums. Now you know more about the most common pitfalls when working with `const` enums, there are more listed on GitHub, but I think the message is clear, `const` enums aren't the solution and in some scenarios should be avoided even more than regular enums. ## If not Enums, then what else? When TypeScript 0.9 (circa 2013) introduced enums there wasn't really any alternative for defining a grouped set of values in a type-safe approach. But since these days, both JavaScript and TypeScript have changed a lot, and now there are alternatives. If you look at the [enum documentation](https://www.typescriptlang.org/docs/handbook/enums.html?ref=angularspace.com#objects-vs-enums) of TypeScript, they also express that in modern TypeScript, you might not need enums anymore, as there now are alternative approaches that are more in line with the state of JavaScript. The approach TypeScript recommends as an alternative is the `as const` object. With this approach, you simply define a `const` object and do an `as const` assertion on the object to seal it and tell TypeScript the values are literal values instead of primitives, meaning, if you define a string value, it will type it as that value instead of a string. Below, you can see an example of our Status enum on an `as const` object form: ```typescript const StatusEnum = { Success: "Success", Pending: "Pending", Failed: "Failed", } as const; // StatusEnum.Success is equal to 'Success' ``` This object will give you your enum, but you can't use this as a type, so in addition to the object, you also need to define a type. Because of that, I like to suffix my `as const` property names with `Enum` to indicate it is the enum and not the Type. In addition to the object, you can create the `Status` type like this: ```typescript type Status = (typeof StatusEnum)[keyof typeof StatusEnum]; // The above code is equal to manually defining a union type: // type Status = "Success" | "Pending" | "Failed" ``` You might be asking yourself, why is this better? For starters, because it's an object, you are required to define explicit values, and no magic numeric values are assigned by the TypeScript compiler. There is also no unexpected bloat of your JavaScript bundles, the object you defined is a regular JavaScript object and will remain unchanged during the compilation, what you see is what you get. All the type information you defined will be stripped during the compilation process. This approach only uses erasable syntax, so it can be used in NodeJs without a TypeScript compiler in between or with libraries and other tools that do not handle non-erasable code. You also don't need a lint rule to restrict the types of your enum values and prevent mixing numeric and string values. You simply append your `as const` object with `satisfies Record` as seen below: ```typescript const StatusEnum = { Success: "Success", Pending: "Pending", Failed: "Failed", } as const satisfies Record; ``` If you want to use numeric values instead, you can use this `satisfies Record`. When using `as const` objects, there are also no inconsistencies between numeric and string values, everything will be handled equally and how developers are used to. Because you're using regular JavaScript objects, you can also do reverse lookups on both string and numeric enums when using the `as const` approach. All the issues related to `const` enums are also irrelevant for `as const` objects because you're defining JavaScript code, which will always be available in the compiled bundles. That basically covers every pitfall of the enum and `const` enum with a smaller footprint on your bundle size. The only thing that can't be covered out of the box with `as const` objects is preventing duplicate values, but since you always have to define your values explicitly, it will not happen unexpectedly as it can with numeric enums. So this is not really an issue and having the option for when you actually need it isn't bad, what is bad is when it happens without your explicitly defining the values. Lastly, with `as const` objects, you can provide valid literal values that are defined on your enum instead of using the enum object (similar to numeric enums, but opposed to string enums): ```typescript const StatusEnum = { Success: "Success", Pending: "Pending", Failed: "Failed", } as const satisfies Record; type Status = (typeof StatusEnum)[keyof typeof StatusEnum]; function setStatus(status: Status) { // set status } setStatus("Success"); // ✅ This works ``` While `Success` is a valid value, and you can't supply invalid values if this is an advantage or disadvantage, it is debatable. In general, I would say it's better if you can only use the enum object to provide `Status` values, although, in a lot of frameworks, Angular being one of them, you can't directly use imported values inside your templates. So, if you want to use an enum value as a function param in your HTML template, you need to assign the enum to a property in your component class before you can use it inside the template. This requires an additional property on your component, further increasing your bundle size (granted very marginally), while you could type-safe pass in the value directly in the template as well. Personally, I'm a bit conflicted on this and can't make up my mind what is correct, but this isn't enough to favor enums over `as const` objects, especially because this is also how it works with numeric enums. All and all I would say the `as const` object does a better job at providing a type-safe approach for defining a set of grouped const values. There are no real drawbacks and issues associated with it, while preventing all the issues associated with TypeScript enums. If you want to prevent the usage of enums in your codebase, you can enforce this with a lint rule: ```typescript rules: { 'no-restricted-syntax': [ 'error', { selector: 'TSEnumDeclaration', message: 'Avoid using TypeScript enums. Use `as const` objects instead.', }, ], } ``` Alternativly if you are using TypeScript 5.8 or higher, you can set the `erasableSyntaxOnly` flag inside your `tsconfig.json`, but this will also disable all other non erasable syntax (which is a good thing if you ask me). ## Conclusion In conclusion, while TypeScript enums have been a valuable tool for developers, they come with several pitfalls and limitations that can lead to unexpected behavior and increased bundle sizes. The introduction of `as const` objects provides a modern, type-safe alternative that aligns better with the current state of JavaScript and TypeScript. By using `as const` objects, developers can avoid many of the issues associated with enums, such as non-erasable code, inconsistencies between string and numeric enums, and declaration merging. If you want to enforce the use of `as const` objects in your codebase, consider using lint rules or the `erasableSyntaxOnly` flag in TypeScript 5.8 or higher. This will help ensure a more consistent and efficient codebase, ultimately leading to better performance and maintainability. Ultimately, the choice between enums and `as const` objects is up to you, but with the information provided in this article, you can make a more informed decision that best suits your project's needs. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/Screenshot-2024-11-13-at-15.52.00--1---6---1---1-.jpg) ### Senior Engineer to Lead - Full Scholarship 1x Giveaway 2nd Edition URL: https://www.angularspace.com/senior-engineer-to-lead-full-scholarship-1x-giveaway-2nd-edition/ Last updated: 2025-04-25T13:49:18.000Z Second Edition of a very special giveaway! _This post is for subscribers only._ ### Meet HTTP Resource URL: https://www.angularspace.com/meet-http-resource/ Last updated: 2025-04-14T08:39:43.000Z In a [previous article](https://www.angularspace.com/everything-you-need-to-know-abour-resource-for-now/), I have covered the new Resource API, which was added in Angular in version 19 as an experimental feature to promote better approaches with reactive programming. If you have not read that article and are completely unfamiliar with the Resource API completely, I suggest you either read the article or familiarize yourself with the API via the new RFCs the Angular team just published ([RFC 1](https://github.com/angular/angular/discussions/60120?ref=angularspace.com), [RFC 2](https://github.com/angular/angular/discussions/60121?ref=angularspace.com)). So, if we are on the same page now, let's explore the newest member of Angular's reactivity family: `httpResource`! ## What is `httpResource`? `httpResource` is a reactive primitive, much like `resource`/`rxResource` from the previous article, but simplified and specifically tailored to work with HTTP GET requests. Why only "GET" requests, one might wonder? Well, we are going to answer this in detail later in the article, so stay tuned! ### The simplest example So, how does `httpResource` look in action? Let's explore this with a simple example: ```ts import { httpResource } from '@angular/common/http'; @Component({ template: ` @if (users.hasValue()) {
    @for (user of users.value(); track user.id) {
  • {{ user.name }}
  • }
} @else if (users.isLoading()) {

Loading...

} @else if (users.error()) { } `, }) export class UserListComponent { users = httpResource(() => 'https://jsonplaceholder.typicode.com/users'); } ``` As we can see, `httpResource` returns a `ResourceRef` just like `resource`/`rxResource`, with the same methods and properties like `isLoading`, `hasValue`, `error`, etc (consult the article mentioned above or the RFCs for the full APIs). But, crucially, we write a bit less code, just calling the `httpResource` function with a callback that returns a URL to which the HTTP call will be performed. But why a callback and not just a string? Let us see. ### HTTP resources depending on signals Now, let us imagine that we are building the "User Details" page, which gets the id of a user via a signal input and fetches the user data from the server. We can use the `httpResource` function to achieve this: ```ts @Component({ template: ` @if (user.hasValue()) {

{{ user.value().name }}

{{ user.value().email }}

} @else if (user.isLoading()) {

Loading...

} @else if (user.error()) { } `, }) export class UserDetailsComponent { userId = input.required(); user = httpResource(() => `https://jsonplaceholder.typicode.com/users/${this.userId()}`); } ``` As we can see, here, we are passing the `userId` signal to the `httpResource` callback, which allows us to fetch the user data based on the signal value. This is powerful, because the callback for `httpResource` is tracking any signals passed to it, so, if, in our example, `userId` changes, the `httpResource` will automatically refetch the user data for the new id from the backend. This makes `httpResource` a great reactive building block, because we can use it simply in one line to create everything we need in regards to HTTP GET requests (which are usually the majority of requests in most applications). > Note: when one or more of the tracked signals change, the `httpResource` will cancel any current HTTP calls and switch to the new one, similar to how `switchMap` works in RxJS. By the way, this is even a more powerful tool when combined with Angular's new component-routing input binding, where we can get parameters from the URL as signal inputs and use them in `httpResource`. If you are unfamiliar with component input binding, check [this documentation page](https://angular.dev/api/router/withComponentInputBinding?ref=angularspace.com), but for this particular example, what we need to do is enable the input binding: ```ts export const appConfig: ApplicationConfig = { providers: [ provideRouter(appRoutes, withComponentInputBinding()), ] }; ``` And then define the parameters for our component's route: ```ts export const appRoutes: Routes = [ { path: 'users/:userId', component: UserDetailsComponent, }, ]; ``` This was, we can 1. Use parameters from URL as if they were component inputs 2. Load data based on parameters 3. Keep everything in sync if the user navigates to another user's page, changing the `userId` parameter Now, let us explore what else does this new resource have in store for us. ## What else does `httpResource` has to offer? As we can see, `httpResource` is working by default with JSON responses - which makes sense, since it is the well-known default for `HttpClient` itself, and `httpResource` is built on top of it. But what if we want to work with other types of responses, like text, blobs, or even custom types? No worries, `httpResource` has an API similar to `input` signals, where we could specify a required input by doing `input.required()`, and we can do the same with the response type: ```ts @Component({ template: ` @if (data.hasValue()) {
{{ data.value() }}
} @else if (data.isLoading()) {

Loading...

} @else if (data.error()) { } `, }) export class RawDataComponent { fileId = input.required(); fileResource = httpResource.blob(() => `my-api.url/files/${this.fileId()}`); } ``` In this case, we would get access to the response as a blob, and can then utilize it using tools like `new File` or `Reader`. Not that we can also get a streamed `ArrayBuffer` using `httpResource.arrayBuffer` or plain text using `httpResource.text`. ## Parsing responses In the modern web, runtime type validation libraries like [Zod](https://zod.dev/?ref=angularspace.com) are now a crucial part of many applications. If you are unfamiliar with Zod, in short, it is a TypeScript-first schema declaration library, which allows us to do things like this: ```ts const userSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); type User = z.infer; ``` Then, we can verify (at runtime!) if certain objects are compliant with a given schema, or parse an object to be assured it is of certain type: ```ts const user = userSchema.parse({ id: 1, name: 'John Doe', email: 'johndoe@example.com'}); ``` One of the popular use cases for Zod and similar tools is verifying HTTP responses; for instance, with Angular's `HttpClient`, we pass an optional type parameter to indicate what kind of response we *expect*, but there is no actual way of verifying it: ```ts this.http.get('https://jsonplaceholder.typicode.com/users/1').subscribe(user => { // user is of type User, but it is not guaranteed to *actually* be this type // we can only hope that the server response is correct }); ``` Instead, many developers currently use Zod to verify the response: ```ts this.http.get('https://jsonplaceholder.typicode.com/users/1').pipe( map(response => userSchema.parse(response)) ).subscribe(user => { // user is of type User, and we are sure that it is correct }); ``` So, `httpResource` makes it way easier to this refinement with Zod or any other validation library by adding a `parse` option when using it: ```ts export class UserDetailsComponent { userId = input.required(); user = httpResource({ url: (id: number) => `https://jsonplaceholder.typicode.com/users/${this.userId}`, parse: userSchema.parse, }); } ``` Now, into the full request API! ## Customized HTTP calls Sometimes, we need to have a different methods for making requests, and while using "POST" to get the data is not a conventional approach, sometimes we just do not have control over the APIs that have to work with; in that case, we can invoke `httpResource` with a configuration object: ```ts export class UserDetailsComponent { userId = input.required(); user = httpResource({ url: () => `https://jsonplaceholder.typicode.com/users/`, method: 'POST', body: { id: this.userId }, headers: { 'Content-Type': 'application/json' }, parse: userSchema.parse, }); } ``` On top of this, the `ResourceRef` we get from `httpResource` is actually a specific `HttpResourceRef`, which extends `ResourceRef` with additional methods and properties. Let's see that in action now. ### Reading headers In some cases, it can be important to access the response headers of a specific HTTP request. For instance, we might want to read the `Content-Type` header to determine how to parse the response. With `httpResource`, we can do this easily: ```ts @Component({ template: ` @if (data.headers().get('Content-Type') === 'application/json') {
{{ data.value() | json }}
} `, }) export class JsonDataComponent { data = httpResource(() => 'my.url'); } ``` The `headers` signal from `HttpResourceRef` is an [HttpHeaders](https://angular.dev/api/common/http/HttpHeaders?ref=angularspace.com) object, giving us the ability to read and check any necessary headers. ### Reading status Sometimes, the HTTP status can also be important, for example, to determine what specific UI to display in case of an error: ```ts @Component({ template: ` @if (data.status() === 404) {

Not found

} @if (data.status() === 401) {

Unauthorized

} `, }) export class StatusComponent { data = httpResource(() => 'my.url'); } ``` ### Download progress When downloading large responses (like Blobs) with `httpResource`, sometimes it can be handy to display a progress indicator. We can do this by utilizing the `HttpResourceRef.progress` signal, which returns an [HttpProgressEvent](https://angular.dev/api/common/http/HttpProgressEvent?ref=angularspace.com) object: ```ts @Component({ template: ` @if (data.isLoading()) {

Downloading...

} `, }) export class UploadComponent { data = httpResource.blob(() => 'my.url'); } ``` > Warning: using the `fetch` implementation of HTTP calls is not capable of sending progress events, so this feature is only available if you do *not* provide the `HttpClient` with the `withFetch` option > Note: as `httpResource` is designed for retrieving data, you should only use it to download, not upload (files, blobs, etc) Now, as we familiarized ourselves with the API and structure of `httpResource`, let's see what downsides and concerns it has (at least at this initial stage). ## What are the concerns? First of all, as mentioned, `httpResource` uses `HttpClient` under the hood, which means that you need to provide it in the application config: ```ts export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), ], }; ``` This is not a big deal, but if you are, for whatever reason, not using `HttpClient` and utilize something different (maybe straight `fetch` or some library), you will not be able to use `httpResource`, and should instead rely on the `resource` building block: ```ts export class UserDetailsComponent { userId = input.required(); user = resource({ request: () => ({id: this.userId()}), loader: () => fetch(`https://jsonplaceholder.typicode.com/users/${this.userId}`), }); } ``` Next, it is important to understand that `httpResource` makes the requests eagerly, whenever created, meaning it can be trickier to chain requests. For instance, you might load a `Product` object and then want to load other products from a "similar-products" endpoint. With `httpResource`, you would need to create a new `httpResource` instance that depends on the first one, and handle the chaining logic by returning `undefined` while the first request is still loading and the data is yet to become available: ```ts export class ProductComponent { productId = input.required(); product = httpResource({ url: (id: number) => `https://my-api.url/products/${this.productId()}`, parse: productSchema.parse, }); similarProducts = httpResource({ url: () => this.product.hasValue() ? `https://my-api.url/products/${this.productId()}/similar` : undefined, parse: productSchema.parse, }); } ``` So, with this, we covered everything we can know about `httpResource` right now, but a lot of it remains to be seen. In the next section, let us explore the future of resources in Angular to understand how the grand logic of HTTP operations will be shaped. ## Going forward with resources The Angular team has made it clear that the signals will be the go-to way of working with HTTP-related operations in Angular apps in the future, and Resources are the current thing that supports a part of it. However, currently, it only covers the half of the equation: getting data from a server. But, as we mentioned, `httpResource` by default uses the "GET" method, and is designed for data retrieval only. Of course, technically, we could force it to use a different method and perform a different operation (like deleting something from the database instead of fetching). However, philosophically, resources are not suitable for this; they are designed as writable computed signals of which the source data is something on the backend. Deleting something, for example, is not "data" and using resources to represent them is confusing. So, what are the options? Currently, the Angular team has not settled on one particular approach, but it hasn't stopped Angular enthusiasts from exploring possibilities; let's take a look at some of them now. For instance, [Tomas Trajan](https://x.com/tomastrajan?ref=angularspace.com) has suggested a concept of `crudResource`, in which all the operations related to a backend entity are encapsulated in a single resource object. Here is an example: ```ts @Component({ /* ... */ }) export class TodoComponent { todos = crudResource('/todos', { strategy: 'optimistic', create: { behavior: 'concat' }, remove: { behavior: 'merge' } }); newTodo = ''; create() { const title = this.newTodo; this.newTodo = ''; this.todos.create({ id: v4(), title, completed: false }); } toggle(todo: Todo) { this.todos.update(todo.id, { ...todo, completed: !todo.completed }); } remove(todo: Todo) { this.todos.remove(todo.id); } } ``` You can read more about it in Tomas' [tweet](https://x.com/tomastrajan/status/1895375197219512825?ref=angularspace.com) or try it out using the code he provided in [this Github Gist](https://gist.github.com/tomastrajan/6433fbf973adfed0c40ad49ecac6a543?ref=angularspace.com). While Tomas' approach seeks to replace the `httpResource` with a higher-level building block to encompass all possible operations, a solution proposed by [Marko Stanimirovic](https://x.com/MarkoStDev?ref=angularspace.com) seeks to expand resources with the addition fo a new `httpMutation` primitive, which would cover everything related to other HTTP operations like "update" or "delete". Here is a quick look: ```ts const updateUser = httpMutation((user: User) => ({ url: `/users/${user.id}`, body: user, method: 'PUT', onSuccess: () => usersResource.reload(), onError: console.error, })); // Execute mutation updateUser.mutate({ id: 1, name: 'Marko' }); // Status updateUser.isPending(); updateUser.isFulfilled(); updateUser.isError(); ``` On top of this, Marko has also proposed an `rxMutation` that leverages the power of RxJS to provide the full range of capabilities that might be necessary when doing such operations: ```ts // Simple Mutation const deleteCustomer = rxMutation((id: number) => customersService.delete(id)); deleteCustomer.execute(123); // With Concurrency Control const updateCustomer = rxMutation({ executor: (customer: Customer) => customersService.update(customer), operator: concatMap, // mergeMap by default for parallel mutations onSuccess: ({ value }) => { console.log(`Customer ${value.name} updated successfully`); }, onError: ({ input, error }) => { console.error(`Failed to update ${input.name}`, error); } }); ``` To better familiarize yourself with these concepts, read Marko's tweets ([here](https://x.com/MarkoStDev/status/1895464855773286891?ref=angularspace.com) and [here](https://x.com/MarkoStDev/status/1896570337866703144?ref=angularspace.com)) and give `rxMutation` a try with this [repository](https://github.com/markostanimirovic/rx-resource-proto?ref=angularspace.com). > Warning: ideas mentioned in this section are just that: ideas proposed by curios members of the Angular community. They may or may not become a part of Angular itself, so be careful when using them in your projects. ## Conclusion Starting from v16, signals and everything else that grow out of them (linked signals, resources, etc) have become an incredibly important part of the Angular ecosystem. With the introduction of `httpResource`, we are now able to handle HTTP GET requests in a more reactive and concise way, which is a great step forward in the evolution of the framework. As mentioned, resources are still experimental, and there is lots of stuff about them that still needs to be added or ironed out. This is why I'd like to encourage readers of this article to also read the RFCs mentioned at the beginning and give feedback and new ideas so we can settle on the best approaches in the future. ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/04/Screenshot-2024-11-13-at-15--1-_upscayl_2x_high-fidelity-4x.jpg) ### Autopsy of super slow test in an Angular Monorepo URL: https://www.angularspace.com/autopsy-of-super-slows-test-in-an-angular-monorepo-2/ Last updated: 2025-03-27T13:39:16.000Z Ah, welcome back to Angular Space, where my articles describes the smooth of processing development pipelines and look at them go to perish (sometimes). In this thrilling third installment (yay! 3 articles!), let's dive into one of the most exquisite nightmares Angular devs are forced to endure from time to time—testing issues, specifically, the sheer agony of long test executions. ### Motivations and goals This article is purely conceptual and, spoiler alert, doesn't have much code. Instead, it dives into the beautiful disaster that is your test startup time, breaking down why your pull request turns into a tragic symphony of misery the moment it hits your CI pipeline. Think of it less as a tutorial and more as a support group for developers questioning their life choices while waiting for their tests to run. ## Recommended reading I would suggest to take a reading to [Deep dive into Nx Affected](https://www.angularspace.com/deep-dive-into-nx-affected/). The reading can overview in some manner how a test might be running in an Nx workspace when things changes, which is perfect for this ~~horror storytelling~~ article. Grab some popcorn, and water (I don't want to choke you). ## Introduction Prior to start, let's start with the setup to observe: - Nx Monorepo (more likely 16, or anything modern if you want) – because managing a single project in a separate repo wasn't soul-crushing enough. - Multiple Angular 16 applications – since leadership is too terrified to pull the trigger on a full migration to LTS... cowards... - Internal libraries galore – the glue that keeps our UI components works as intended functioning across the workspace... A happy salad of code courtesy of your transcontinental team efforts. - Third-party organizational UI dependencies – an unholy mix of Angular 14 and 16, because consistency is for the weak. Poorly architected. it works as intended for some miracle. - Everything is beautifully written in TypeScript – or so we tell ourselves. - Testing is handled by Jest – courtesy of Nx's "community support" since Nx 14, Jasmin was not fun. and here we go... It's 9:00 AM. You drag yourself to your desk. You turn on your PC. You sip your coffee from that “Official Angular Space” mug—except it is not an official one. No, because the Angular Space dude never sent one to you. So, in an act of sheer defiance, you took a permanent marker and made your own from that purple mug courtesy of some cellphone provider. Artistic genius. Then, you open your inbox. Emails flood in from your transcontinental teams. The build times are horrendous. They rage, weep, suffer. Dante's hell is small in comparison to this. Some common task on the pipeline, like `nx affected -t test`, should take mere seconds… but no. No. They take 15 to 30 minutes just to run some trivial tests. You glance at your mug. The coffee inside has grown colder. It sits at the edge of your desk now, trembling under the weight of your impending despair. You check the logs. Nothing. The Jenkins execution times glare back at you like a cruel joke. That cold feeling in your legs is not like you don't exercise, but the starting symptoms of panic (A feeling that by know you know very well). ## DAMN THAT IS SLOW! Tests—those seemingly innocent creatures—have somehow mutated into monstrous entities that take minutes just to wake up, only to sprint through execution in mere seconds. No errors, no warnings—just soul-draining startup times that make no sense. And yes, even if you're using `nx cloud`, with your army of Nx agents working in parallel, some unlucky ones might still experience the delightful mystery of tests that crawl at an inexplicably slow pace for no apparent reason. Why should anything be predictable? So, let's put on our detective hats and investigate: Why the hell is a simple 3 case unit test taking five whole minutes (or even more) just to start? ## How Nx does testing with Jest By now, your coffee has gone cold, your colleagues are angrily DM'ing you about "Why are tests so slow?" and you're questioning why you became a developer in the first place. Let's take a closer look at how Nx Framework handles testing with Jest to understand why time—and your coffee—slips away before the tests finish. It's a process that works but serves as a stark reminder of how fleeting and precious time really is. ## TD;DR : What Nx Does with Jest - Animated Version Probably you're a visual learner, and of course I don't forget about how you interpret information. This chart was specifically designed to assist you on creating a mental map of the process in a general overview. It will easily point the process on how Nx and Jest work along on testing—and avoid in the process of reminding you whatever problem you're getting right now on your slow tests. Pick your poison. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/ezgif-66e808f2fabd19.gif) Nx Test using Jest, abridged version ### Test Code Transpilation. Now that you have decided to understand what is happening behind scenes, it will be a good reminder to pick those blue latex gloves, a surgical coat and a broom/dust pan combo from your kitchen to check what is happening one step at the time with your slow tests. After all, this is an autopsy, and not the scene of a crime. When you run `nx test` in a library configured for testing with Jest (check `project.json`), Nx uses `@nx/jest` to run `ts-jest` on your tests on setup to transpile code. Because one round of transpilation isn't enough, Nx ensures that every time Jest runs, it has to transpile your TypeScript code to run the test. Once the test passes, the outcomes of the test are stored on the Nx Cache until you hit `nx reset` just to grace your day to rerun things from scratch (`test` and `build` targets tends to be cached when those run successfully... but not here). Forget the built code in the first CI pass that usually is `nx affected -t build` which usually is generated on the `dist` folder, because Jest insists on handling it itself. Why not, right? It's not like you had anything better to do with those extra minutes. And what you do with those extra minutes? of course you could memory profile the test! For example, you could determine the actions prior to test, or during the test. While time passes on your profiling, Jest ends up in setting an spectacular graph figuring out stuff: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/1_bLa7-pVZDFMkNMiY5j1ohg.webp) A glorious graph of a jest test running under 6 seconds. Imagine your 30 minute test here... ### Workspace test preset The Workspace Configuration Relies on a preset file which usually is `jest.preset.js`. This mystical file is the core configuration for your workspace, dictating how tests across separate apps and libraries should behave. It's like a set of laws written on stone tablets. Nothing divine about it or what it is written on it. ### Jest Treats Each App/Library as an Island Jest isolates apps and libraries during testing by design. When you set Jest as the test runner for an application or library, Nx doesn't just shrug and leave you hanging. Instead, it kindly prepares specific configurations, sprinkling magic dust (read: boilerplate) so everything "just works". It is rigid enough to make every test to play the same rules... but hey, on the bright side this ensures no cross-contamination between tests while in a darker side it adds a layer of overhead when working in a monorepo. ### Your `test-setup.ts` and `jest-preset-angular` The `test-setup.ts` file includes a reference to `jest-preset-angular`, which is a package that contains the baseline for running Angular-specific test which is plain and generic on Apps and libraries. Unless you want something quite specific (like defining some global configuration for a specific library/app), the `test-setup.ts` is a sad photocopy on each app/library that uses it. ### The `test` Executor in project.json Located under the test target in your `project.json`, the executor points to `@nx/jest`. It acts as the middleman, passing the workspace preset configuration and whatever additional parameters you throw at it. Think of it as a reluctant courier—you give it a package, and it delivers it (eventually). ### The Executor Uses Jest to Transpile and Run Tests And here's the kicker: the executor calls Jest, which then transpiles your code AGAIN to run the tests. Because why settle for one round of transpilation when you can have two? It's the gift that keeps on giving (your CI pipeline gets an extra headache, and a round up of last minute on the architecture meeting call). As a result, if you dive good enough, it creates a `/jest` folder in your system `tmp` folder, where all the transpiled code will live. ### Third-Party Dependencies and Modules Now that we have a clear picture of how Nx, Jest, and Angular interact, let's talk about those monolithic third-party packages that somehow always find a way to make your life harder. Remember those Angular 14 libraries we mentioned earlier? The ones clinging onto life in your workspace? Well, In this case, every single component (or sometimes hundreds of them) lives inside modules. And here's where things get interesting—or, depending on your mood, miserable. See, in an attempt to "simplify" usage, some brilliant decisions lead to absurdly massive modules being passed around like a family heirloom. The problem? Each time a test runs, it needs to resolve the module and all of its components. Yes, every single time. If Jest doesn't cache it, grab some popcorn and enjoy the slow-motion train wreck of unnecessary resolutions. ## ts-jest and Code Transpilation When large modules are referenced in a test for a component without being passed or mocked, Jest doesn't just ignore them. No, it transpiles everything. And because we love torturing ourselves, let's verify what's happening: Run this command at the root of your workspace: `node ./node_modules/.bin/jest --cleanCache` This will return the cache directory Jest uses. ***Keep an eye on it***, literally. You will see. Now, run your problematic test. Because if you suddenly see it growing like an unchecked memory leak, well, congratulations—you've found one of the many reasons your tests are moving at glacial speed. Note: Did I Say Memory Leak? YES. One of the main reasons a test exits without any reason in Jest is that the sheer amount of transpiled code becomes too much for it to handle. On CI world, Jest in its desperate attempt to process something it was too much to manage, eventually just gives up. No error message. No logs. No graceful failure. Just… nothing. It's like watching someone stare into the abyss—except, in this case, the abyss stares back, then quietly shuts down your test run without so much as an exit code. ## One Test File. Three Test Cases. Five (or more) Minutes. Now, let's address the elephant in the room which is more likely A potential memory leak. Let's assume your component is purely presentational. No heavy logic. No complexity. Just an SSS component (Super Simple Stupid). So why, in the name of everything sacred, does this test take 5 minutes to start? Well, let's park the memory leak theory for a second. Upon further analysis, yeah, it could be the case. But there's another, even more frustrating possibility. When `ts-jest` starts transpiling ALL THE CODE related to the component at test startup, you'll notice a tree of multiple files and folders forming around at great speed on ***THE FOLDER I TOLD YOU TO WATCH***. It's like watching a horror movie where every new scene brings another terrible revelation. Normally, you'd use `transformIgnorePatterns` on your Jest configuration to skip unnecessary files from being transformed. But in cases like these, the sheer number of files being processed is absurd. The deeper you dig, the more you uncover a sprawling, tangled mess of transpiled dependencies. Suddenly, deep inside Jest's transpilation logs, you recognize some familiar faces on your codebase: - Ag-Grid? Yep, you see some code for cell renderers, a familiar API implementation here and there, and recognizable functions you use for your grids. fantastic. - Angular Material? Of course. - Random three-line transpiled files that serve no visible purpose? You bet. - And that component that somehow got clustered on the module of that UI library, but is not being used at all ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/jest.png) An example of ag-grid after transpilation of one of its many encapsulated methods on an Angular test that used it. The list of files being generated doesn't end for a test that small. And the worst part? The more dependencies Jest has to transpile from such modules, the bigger the memory load. It is a three (3) case test for God's sake! and the amount of files generated on the transpilation seems not to stop. The clock keeps ticking, and your anxiety skyrockets, as the terminal doesn't show any visible sign that it ended the process to start doing something. This is where things start breaking in CI. On `nx affected -t test`, you might **not even get an exit error code**—just a void. No output. No explanation. Nada. Some affected apps and libraries show results, but that problematic library or app responds with sheer silence in defiance to your hard planned work, as if Jest itself has given up on life. It's like the old tale of the milkmaid—she carried too many jugs of milk, only to end up with everything shattered on the ground. Except here, it's your CI pipeline, and the spilled milk is your tears of fully justified frustration. Now, lets isolate the failing app/library by first executing again ``` node ./node_modules/.bin/jest --cleanCache` ``` and ``` nx reset ``` (Why I would keep the cache of something failing?) in this way, we should be able to track down the output of: ``` nx test that-f-library ``` If everything works fine, You would witness, in full terror: - A 5-minute (or more) startup time for the test suite. - A 3-test suite that executes and passes in mere milliseconds. - and an absurd amount of transpiled files being generated on the jest cache ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/ongoing.png) Jest cache transpiled for a middle size application, with an unholy amount of dependencies... in 3 minutes (it keeps going...) How do we fix this? Ok, Time for a break. Step away. Breathe. Stare into the void. Question your life choices. Because clearly, something is very, very wrong. ## The Real Question: Why Aren't We Testing Built Code? Let's step back for a moment. By looking at the pipe line process, and observing this in a holistic manner, an obvious thought emerges... If we already run on CI: ``` nx affected -t lint ``` and ``` nx affected -t build ``` ... it effectively produces in a reasonable time fully transpiled, production-ready code right? ... so ... Why to test the source TS code when we can test the built code instead? Wouldn't that at least reduce some of this transpilation madness? Wouldn't it prevent Jest from processing dependencies it doesn't need to touch? It's almost as if we're making this harder on ourselves for no reason. But let's dig deeper before assuming anything. ## Barrel files??? Some folk told me back in the day about the benefits of barreling files as if it were an NgModule, which by now it is not the best choice to encapsulate and distribute components, services, etc. However, there are some folks that believe in the principle of encapsulation as their bible, and they place a significant amount of barrel files to "encapsulate" the distribution of everything you develop in a library, like the bible, it has an index of books, chapters, verses, etc. Something thick to read in your free time. Genius analogy. Sadly, we are talking about Jest, and Jest like many other test runners ~~are atheists~~ doesn't keep a bookmark on barrel files like you would do when you read a big book. Instead it decides to cook itself into the references of each content of the imports of such barrel file, and despairs makes its entrance as Jest will crawl those barrel files searching for dependencies and transpile each one of those without mercy like a real Pokémon Master in an animal shelter. And like that, Jest will check its Pokédex. Oh! Not having a transpiled Piggeon in your cache? catch it and transpile it. It was super effective. Won't believe it? here some articles courtesy of some mysterious reviewer which name can't recall at the moment (anyways, you see the reviewers of this article at the bottom of this page. Refined gentlemen.) - [What We have been complaining: slow start times](https://github.com/jestjs/jest/issues/11234?ref=angularspace.com) - [Stop! Won't somebody please think of the... tests?](https://tkdodo.eu/blog/please-stop-using-barrel-files?ref=angularspace.com) - [who asked for an extra cup of barrel files?](https://dev.to/fogel/potential-issues-with-barrel-files-in-jest-1nkl?ref=angularspace.com) ## Speeding things up? or formula for disaster On paper, testing pre-transpiled code sounds like a great idea. The code is modularized, optimized, and should, in theory, run faster. So, what if we just told Jest to use `moduleNameMapper` to resolve everything to the already-built output? Boom—problem solved, right? **Wrong.** It turns out, Jest wasn't really built with this in mind, and blindly pointing `moduleNameMapper` to transpiled code can backfire spectacularly. Instead of skipping unnecessary transformations, Jest might decide to take the scenic route, resolving modules **AGAIN** in a way that actually increases test execution time. That's right—what was meant to be a performance hack could turn into a **slow-motion disaster**. So, before we set the entire testing pipeline on fire, let's consider what actually needs to be done: - Keep running tests in local development using source files to ensure nothing explodes before deployment. - Only in CI, build transpiled code once and test against it—this avoids redundant work and keeps Jest from going full meltdown mode. - Get `moduleNameMapper` right, or else. Misconfiguring it will have the opposite effect, forcing Jest to resolve and transpile dependencies in the most inefficient way possible. - Adjust `transformIgnorePatterns` wisely. Jest is notorious for deciding, on a whim, that something needs to be transpiled—[even if you explicitly told it not to](https://jestjs.io/docs/configuration?ref=angularspace.com) - **Accept that Jest might not be the hero we thought it was.** All that fanfare about speed and efficiency? Yeah, not when you're dealing with monorepo and large-scale Angular projects So, does this approach actually speed things up? Potentially—if done flawlessly. If not, well... you might just end up watching your CI logs go blank as Jest spirals into an existential crisis. Your call. But let's not fly too close to the sun and get burned out in the process. Which such a gamble approach (honestly, this approach is quite risky and should be out of question), it is better to re-evaluate our options. Definitively this might not be a good idea, and if you have read up to here, you and me would agree that we shouldn't take this route. We software engineers are resilient beasts, ready to hunt our pray in many different ways. We can toss this idea to the trash and let's consider another approach: If something that is being tested is including a large set of pieces of code, we might have to focus on how to make those pieces small enough for our test to not choke under the sightless pressure, and that is mocking. ### Mocking strategy So, if Jest is dead set on making our lives miserable by transpiling half the world every time we run tests, the next best move is to trick it into doing less work. Enter mocking strategies, where we convince Jest that it doesn't really need to process those massive, bloated dependencies in full. Here's how we fight back: #### 1\. Mock Third-Party Libraries with `moduleNameMapper` (Properly This Time) We've already established that `moduleNameMapper` can make things worse if misused, but if done right, we can redirect Jest away from unnecessary transpilation. Instead of letting Jest resolve a UI library (like Ag-Grid or Angular Material) and transpile it from scratch, we can mock those libraries to return a lightweight stub. At global scope you can do something like this in your Jest configuration, so we address the biggest animals in the room first: ```js module.exports = { moduleNameMapper: { "^ag-grid-angular$": "/jest-mocks/ag-grid-mock.js", "^@angular/material$": "/jest-mocks/material-mock.js", }, }; ``` Then we create mocking files like `jest-mocks/ag-grid-mock.js` ```js module.exports = { AgGridAngular: jest.fn(() => ({ onGridReady: jest.fn(), api: { sizeColumnsToFit: jest.fn(), }, })), }; ``` 🎯 Why This Works you ask? : Jest will no longer waste time trying to transpile entire UI component libraries—instead, it gets a lightweight stub that satisfies imports but does nothing else. This is nice to use, but if we were testing something specific, we might need to address it in the individual test file. #### 2\. Use `jest.mock()` in Individual Tests For dependencies that don't need to be loaded at all, we can simply mock them per test file instead of forcing Jest to deal with them. Assuming we need to mock a service, we can directly instantiate the name of the service and mock it. ```js jest.mock("src/app/services/heavy-service", () => ({ HeavyService: jest.fn(() => ({ fetchData: jest.fn().mockResolvedValue([]), })), })); ``` 🎯 Why This Works: Jest doesn't try to import the actual service, reducing unnecessary module resolution. It is nice that we can pinpoint to services, but what about doing something more selective? You want to address vulnerable points instead of taking the animal on the room in its enterity. #### 3\. Mock Entire Modules Using `jest.spyOn()` (For Selective Mocking) Sometimes, you want to use part of a module while preventing Jest from touching its heavy dependencies. Enter `jest.spyOn()`, which lets you hijack just the parts you need. for example: ```js import * as HeavyModule from "src/utils/heavy-stuff"; jest.spyOn(HeavyModule, "computeSomething").mockImplementation(() => 42); ``` 🎯 Why This Works: The rest of the module remains intact, but Jest doesn't need to evaluate `computeSomething` or its dependencies. #### 4\. Use `transformIgnorePatterns` to Keep Jest Out of Trouble If Jest insists on transpiling things it really shouldn't, tell it to back off using `transformIgnorePatterns`. For this to go right, you might want to get a refresher on Regex. The idea here is to **ignore specific files or directories to be transformed**, nothing else: ```js module.exports = { transformIgnorePatterns: ["node_modules/(?!(lodash-es|date-fns)/)"], }; ``` in this case, transpile everything in whatever is imported in `node_modules`, except `lodash-es` and `date-fns` #### 5\. Mocking Angular Modules (Stop Loading Everything) One of the worst offenders in test performance is Jest trying to resolve entire Angular modules for every test. Instead of loading a full module, create a lightweight mock module: Example (Mocking Angular Modules in Jest) ```ts import { NgModule } from "@angular/core"; @NgModule({ providers: [ { provide: HeavyService, useValue: { fetchData: jest.fn().mockResolvedValue([]) }, }, ], }) export class MockModule {} ``` Then, in your test: ```ts TestBed.configureTestingModule({ imports: [MockModule], }); ``` 🎯 Why This Works: Jest skips loading the entire real module, reducing initialization times and dependency resolution overhead. #### **ngMocks** Mocking is essential for maintaining **some** level of sanity in testing, keeping our CI pipeline from spiraling into a black hole of unnecessary computations. However, the real tragedy begins when **libraries, services, or components change**—because then, we have to **remock everything**. And let's be honest, nobody reads breaking change logs before installing updates. This is where **`NgMocks`** is really effective: It **automates the nightmare** of manually maintaining mocks, transforming existing components, services, and modules into **mock counterparts**—saving us from our incompetence on mocking something properly most of the times. `NgMocks` is **so extensive**, I could probably write another article about it… but since I'm enjoying my well-deserved **OOO time**, I'll leave that task for another day. Instead, I'll direct you to [**this page**](https://ng-mocks.sudo.eu/?ref=angularspace.com) so you can **educate yourself** and combine `NgMocks` with the horrors—*I mean*, knowledge—you've gained through this narrative. Trust me, it's worth your attention. --- ## **Conclusion** There are **many reasons** why tests slow down in CI. One of the biggest culprits? **Not keeping effective mocking strategies that result in a successful transpilation of test artifacts that can be easily cached.** If you don't store artifacts, you might as well **close your eyes, forget you read this article, wander around the internet looking for solutions, and end up here again—experiencing a strange sense of déjà vu.** Keeping artifacts isn't just a convenience—it's **the** way to ensure your test suites receive the performance boost they desperately need. And since **Nx Cloud** was our tool of choice in this article, using a **private cloud instance** could provide even greater improvements for large codebases, ensuring faster test runs that don't feel like a **medieval torture method**. Now that I've finally **woken up from this fever dream**, I can return to my desk, sip my coffee, and check that my **test artifacts are intact and working**. …Until the next nightmare begins. Thank you for reading. I hope this **stirs up some deeply buried memories you'd rather not revisit**—but hey, it's always good to keep an eye on the abyss, just in case. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/Screenshot-2024-11-13-at-15.52.00--1---2---1-.jpg) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/angular-university-banner-4-1.jpg)](https://angular-university.io/?ref=angularspace.com) ### ng-India Conf tickets 3x Giveaway! URL: https://www.angularspace.com/ng-india-conf-tickets-3x-giveaway/ Last updated: 2025-03-25T13:44:54.000Z Today I have a very special giveaway! _This post is for subscribers only._ ### Facade Pattern in Angular URL: https://www.angularspace.com/facade-pattern-in-angular/ Last updated: 2025-03-19T10:18:16.000Z Creating software is not a one-time process. Over time, and as technology evolves, business requirements also change. Our main goal should be to prepare the application in such a way that we can easily and at a low cost respond to new demands. In this context, code organization becomes a crucial design element that is often forgotten, leading to numerous problems and extending the time required for modifications. One solution to these problems is the **Facade Pattern**. ### Case Study - The "User List" Component Before we start, I'd like to emphasize something critical. Regardless of the framework and tools being used **the frontend should be as simple as possible**. We don't implement core business logic here, our primary goal is to provide a user friendly tool to display the required data and an interface that allows users to trigger business actions. To illustrate this approach, let's consider a component that displays a list of users. We base it on a data source from which we fetch our users. Unfortunately, the user of our application might not be familiar with the data format returned by our data source, so we want to present this data in a readable format (f.ex by tables). On the other hand, we also want to allow the user to interact with this data. User should have access to block, delete, or modify user or to filter users based on certain criteria. For this purpose, we will use our data source to make these actions. For this article, I have prepared a simple component that displays a list of users fetched from an API. In this component, we can also interact with the data through actions like change the displayed page, preview and delete data. ```typescript // service to handle api requests import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; export type User = { id: number; firstName: string; lastName: string; gender: 'male' | 'female'; email: string; phone: string; }; @Injectable({ providedIn: 'root', }) export class ApiDataService { private httpClient = inject(HttpClient); getUsers(page = 1, perPage = 20) { return this.httpClient.get<{ users: User[] }>( `https://dummyjson.com/users?limit=${perPage}&skip=${ perPage * (page - 1) }` ); } } ``` ```typescript // user-list.component.ts import { Component, signal, inject } from '@angular/core'; import { rxResource } from '@angular/core/rxjs-interop'; import { combineLatest, switchMap } from 'rxjs'; import { ApiDataService, User } from '../api-data.service'; @Component({ selector: 'app-user-list', template: ` @for (user of users()?.users; track user.id) { }
#ID Firstname Lastname Gender Email
{{user.id}} {{user.firstName}} {{user.lastName}} {{user.gender == 'male' ? '♂️' : '♀️'}} {{user.email}}
{{currentPage()}}
`, styles: `/** ... **/`, }) export class UserListComponent { private apiData = inject(ApiDataService); currentPage = signal(1); perPage = signal(10); users = rxResource({ request: () => ({ page: this.currentPage(), perPage: this.perPage(), }), loader: ({ request }) => { const { page, perPage } = request; return this.apiData.getUsers(page, perPage); }, }); // actions showUser(user: User) { // ... } removeUser(user: User) { if (confirm(`This operation will remove user #${user.id}. Are you sure?`)) { // ... } } nextPage() { this.currentPage.update((i) => i + 1); } prevPage() { this.currentPage.update((i) => { return i - 1 < 1 ? 1 : i - 1; }); } } ``` At first look, this component could be used in a production application, as all the required functionalities are implemented, so we can deploy the code and close the task, right? Well, not quite... Let's look at the problems with this component. First and foremost, although it may sound strange, this component does too much! Our task is to display data, and the component does that, but it also handles API connections, manages pagination, and handles user actions. This is not a huge component, but further development of it and the implementation of new functionalities will make those problems bigger. We need to implement user preview, and we'll have to decide whether that will be a new page, a popup, or something else. The same about deleting a user—we would need to display a confirmation popup, then, after acceptance, send a request to the API and refresh the list. There's a lot of code ahead of us and many dependencies. The second problem is related to testing - writing unit tests for this kind of component can be a nightmare of mocking every possible element, from API requests to popups, redirects, etc. Changing our data source or its format might require rewriting all logic to accommodate it. ### Facade as a solution Quoting someone's smarter than me, here is an [*explanation of the Facade pattern from refactoring.guru*](https://refactoring.guru/design-patterns/facade?ref=angularspace.com): *A facade is a class that provides a simple interface to a complex subsystem that contains lots of moving parts. A facade might provide limited functionality in comparison to working with the subsystem directly. However, it includes only those features that clients really care about.* *Having a facade is handy when you need to integrate your app with a sophisticated library that has dozens of features, but you just need a tiny bit of its functionality.* In short, a facade is a pattern that isolates certain shared elements in one place and provides an interface to interact with them. And here I'd like to reiterate my words from the beginning of the previous paragraph, **the frontend should be as simple as possible**. In this case, we'll aim to make our component "dumb." Let's try to make it responsible **only** for displaying data. Angular Components stand out among other framework elements (Directive/Pipe/Service) because they have a *template*, the primary task of a component is to display data, so Component doesn't need to know how to communicate with the backend, where the data comes from or how actions are performed. Our main task during the implementation of the facade is to **split our component into two parts. First one where we'll keep the data related to the logic, and the other where we'll use that data and trigger actions**. In our example, our facade will be responsible for: - Executing API requests for data; - Managing state—which page we're currently displaying and how much data we want to display; - Providing functions that allow for state modification (nextPage, prevPage) and data interactions (view, delete); - Storing additional dependencies such as a possible popup/router—logic not directly related to displaying elements, so that our view (component) has minimal dependencies; Our view will be responsible for: - Displaying data; - Triggering functions; Let's modify our example code. The simplest way is to create a new service that we'll inject into our component. ```typescript import { inject, Injectable, signal, computed } from '@angular/core'; import { combineLatest, switchMap } from 'rxjs'; import { rxResource } from '@angular/core/rxjs-interop'; import { ApiDataService, User } from '../api-data.service'; @Injectable() export class UserListFacade { // #1 private apiData = inject(ApiDataService); private currentPage = signal(1); private perPage = signal(10); private userQuery = rxResource({ request: () => ({ page: this.currentPage(), perPage: this.perPage(), }), loader: ({ request }) => { const { page, perPage } = request; return this.apiData.getUsers(page, perPage); }, }); // #2 paginationData = computed(() => { return { page: this.currentPage(), data: this.userQuery.value()?.users, }; }); // #3 nextPage(): void { this.currentPage.update((i) => i + 1); } prevPage(): void { this.currentPage.update((i) => { return i - 1 < 1 ? 1 : i - 1; }); } displayUserDetails(user: User): void { // ... redirect OR display modal with user data } deleteUser(user: User): void { if (confirm(`This operation will remove user #${user.id}. Are you sure?`)) { // ... make request to delete user } } } ``` This is our facade. As mentioned earlier, it's a simple Angular Service to which we've moved the "more important logic" from the component. We have applied some changes to it. In point *#1*, we set the main data as private values - I want to be sure that functions *nextPage* and *prevPage* (point *#3*) are the only ways to update information from outside this service. In point *#2*, I've added one value, a signal with API data and information about which page is currently displayed - this is the data we will make available to our component. Additionally, it's in this facade that I want to have the ability to initiate viewing and deleting a user, which I plan to implement "soon." As you can see, these functions return type are marked as `void`. Why? Because, by design, `UserListComponent` only initiates an action, and how that action is performed should not concern this component, that's what the facade takes care of. And what about informing about the success of the operation or errors? What if we want to implement a "loader" to inform the user that an action is in progress? All of this information should be in our facade, and it should provide the state, which we use in our view to display the necessary elements based on the facade's state. After isolating the logic, it's time to connect our component with the facade: ```diff // user-list.facade.ts @Component({ selector: 'app-user-list', template: ` @for (user of paginationData().data; track user.id) { }
#ID Firstname Lastname Gender Email
{{user.id}} {{user.firstName}} {{user.lastName}} {{user.gender == 'male' ? '♂️' : '♀️'}} {{user.email}}
{{paginationData().page}}
`, styles: `/** ... **/`, + providers: [UserListFacade], // #1 }) export class UserListComponent { + private userListFacade = inject(UserListFacade); - private apiData = inject(ApiDataService); - private currentPage = signal(1); - private perPage = signal(10); - - users = rxResource({ - request: () => ({ - page: this.currentPage(), - perPage: this.perPage(), - }), - loader: ({ request }) => { - const { page, perPage } = request; - return this.apiData.getUsers(page, perPage); - }, - }); - - paginationData = computed(() => { - return { - page: this.currentPage(), - data: this.users()?.users, - }; - }); + paginationData = this.userListFacade.paginationData; // #2 // #3 // actions showUser(user: User) { this.userListFacade.displayUserDetails(user); } removeUser(user: User) { this.userListFacade.deleteUser(user); } nextPage() { - this.currentPage.update((i) => i + 1); + this.userListFacade.nextPage(); } prevPage() { - this.currentPage.update((i) => { - return i - 1 < 1 ? 1 : i - 1; - }); + this.userListFacade.prevPage(); } } ``` In point *#1*, we inject the facade into the component, in point *#2*, we refer to the data from the facade and use it in the view. Additionally, every action (functions in point *#3*) to update this data is called from facade. Looking at this component, we cannot know where the data comes from, what happens when we trigger actions (like how user deletion works). Should this component know that kind of information? No, and in this case, **that is a good thing**. ### Unit Testing Unit testing is one of the most important things to focus on when creating software. Automatic verification of whether our code works properly and whether our later changes do not break the current logic is an underrated tool, even in frontend development, where we have to rely on "physical" interaction with our application. The facade pattern and splitting our code into a view and logic, even though we have to maintain two separate code places, simplifies testing. Why? Because we don't focus on the component as a whole (where we would have to test both the component's state and interactions with it) but on its individual elements - the view and the business logic. Tests for components using the Facade should focus on: - Testing the business logic, where performing functions from the facade should update its state, and how this state should be verified in the tests; - Testing the view, which is how our component view looks depending on how the facade's state is presented; At first looks, there is no differences from those we would face in components that do not implement the facade, but the more dependencies our component has and the more complex the logic we have to implement, the more we appreciate the simplicity we can leverage. Here's what tests for our component could look like: ```typescript // #1 const simpleFacade = { paginationData: signal({ page: 1, data: [generateSimpleUser(), generateSimpleUser(), generateSimpleUser()] }), nextPage: jasmine.createSpy('nextPageFn'), prevPage: jasmine.createSpy('prevPageFn'), displayUserDetails: jasmine.createSpy('displayUserDetailsFn'), deleteUser: jasmine.createSpy('deleteUserFn'), } describe('UserListComponent', () => { let component: UserListComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserListComponent] }) // #2 .overrideComponent(UserListComponent, { set: { providers: [ { provide: UserListFacade, useValue: simpleFacade } ], } }) .compileComponents(); fixture = TestBed.createComponent(UserListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); // #3 it('should be created', () => { expect(component).toBeTruthy(); }); // #4 it('should display table with rows depends on `vm().data properties', () => { let tablesRow = fixture.nativeElement.querySelectorAll('table tbody tr'); expect(tablesRow.length).toEqual(3); simpleFacade.paginationData.update((paginationData) => { return { ...paginationData, data: [ ...paginationData.data, generateSimpleUser(), generateSimpleUser(), generateSimpleUser(), ] } }); fixture.detectChanges(); tablesRow = fixture.nativeElement.querySelectorAll('table tbody tr'); expect(tablesRow.length).toEqual(6); simpleFacade.paginationData.update((paginationData) => { return { ...paginationData, data: [] } }); fixture.detectChanges(); tablesRow = fixture.nativeElement.querySelectorAll('table tbody tr'); expect(tablesRow.length).toEqual(0); }); // #5 it('should call facade `nextPage` when user click on `nextPage` button', () => { const nextPageButton = fixture.debugElement.query(By.css('button.nextPageBtn')); nextPageButton.nativeElement.click(); expect(simpleFacade.nextPage).toHaveBeenCalled(); }) // #6 it('should call facade `prevButton` when user click on `prevButton` button', () => { const prevPageButton = fixture.debugElement.query(By.css('button.prevPageBtn')); prevPageButton.nativeElement.click(); expect(simpleFacade.prevPage).toHaveBeenCalled(); }) ``` At the very beginning (*#1*), I create an object that imitates the facade, this is the most important object for my view, so I need to have control over it in the tests. Note that I didn't implement the actual methods from the facade here, I use `createSpy` method from jasmine." In the component, I don't care what these functions are doing. As the initiator of these actions, my question is: are they triggered from the component level (after a button click in points *#5* and *#6*). In point *#2*, I override the facade in the component with the one I created for the tests. In point *#3*, I verify whether my component is created. Point *#4* deserves a separate explanation. Here, we verify whether the number of rows in our view matches the number of data entries in our facade. Initially, it's 3, then (by updating the signal) I add 3 more elements, and finally, I check if no rows are displayed when the data is empty. This is the essence. In a very simple way, by updating the data in my mock, I can check if my component behaves correctly. My production facade requires an API request and a change in the page count signal to update this data. If I hadn't split the component and facade, I would have had to mock the ApiDataService or manually set the data by referring to the instance of the component in the test. And Yes, it's ok, but it doesn't make sense because after all, the end user only changes this data by clicking buttons! ### Conclusion Should I always use this pattern from now on? Not really. In what cases should I implement it? *It depends.* As an example of implementation, I used a popular functionality - pagination, making API requests, and displaying elements. This is a task that most of us have likely encountered, and I aimed to show how to prepare a component properly and one of many methods for isolating certain logic into a facade. Using this pattern in this case could be like using a sledgehammer to crack a nut. One of the key risks when implementing the Facade Pattern is inadvertently creating a "[God Object](https://en.wikipedia.org/wiki/God%5Fobject?ref=angularspace.com)". This happens when a single facade becomes responsible for too many tasks, turning into a large, unwieldy structure that tries to manage all business logic, state, and dependencies. When creating a facade, you should: - **Ensure that your facade doesn’t take on too many tasks.** The primary purpose is to create a simple model dedicated to a specific functionality that *helps* in management rather than replacing the implementation of business logic. Facade should focus on a single area, providing a clear and minimal interface. By doing this, you not only improve maintainability but also make your facades easier to test and reuse in other parts of your application. - **Minimize the facade’s interface.** Expose only the methods/values required by external application components. One of the reasons for implementing a facade is to hide technical aspects of the code and dependencies. Methods in the facade should describe *what they do* rather than *how they do it*. Good implementation of this pattern makes our code more flexible! A once-defined facade can be incredibly helpful when migrating solutions used in a project. Let’s assume you're creating a *proof of concept* of a new feature that fetches and performs certain backend requests, which is still under development. By using a facade, we can implement the functionality using sample data (even hardcoded ones) and continue our work without being blocked by missing backend. Afterward, we only need to replace the right methods in the facade to make API requests instead of returning sample data. The same solution applies if we want to change the technology. For example if our component uses data from a Global Store `@ngrx/store`, and for some reason, we decide to drop it or we plan to replace API requests from REST to GraphQL we just need to focus development on the facade and make changes there. We know exactly what data our facade must provide to other components, what methods these components call, and what they are supposed to execute. We can modify the technology as needed while maintaining the expected results. As always, if you're interested in the complete code for the component, I encourage you to play around with [my example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-bwovfj?file=package.json&ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/Screenshot-2024-11-13-at-15.52.00--1---6-.jpg) ### My Personal Take On Signal Types In Angular URL: https://www.angularspace.com/my-personal-take-on-signal-types-in-angular/ Last updated: 2025-03-18T05:48:47.000Z On May 3, 2023, signals were introduced in Angular v16 as a reactive variable for managing application state. While it was a new feature at the time, by Angular v19.2, signals have already become a well-established API. Although not all legacy corporate projects have adopted signals, most developers are aware of them (whether in a positive light or not). In the latest (currently v19.2) we have signal APIs such as `httpResource`, `rxResource` / `resource` &`linkedSignal`. In this article I want to give my thoughts on signals, how I look at signals, in which situation I use them, and how they compare to alternative approaches, such as RxJS solving the same problem. ## Signal Primitives [Reading Angular’s documentation](https://angular.dev/guide/signals?ref=angularspace.com), they describe that `signal()` is a wrapper around any primitives or complex data structure that notifies consumers when the value changes. I use `signal()` whenever a variable is used in the DOM and its value updates over time. A common example is toggling between components using two buttons to control which component is displayed. ```TS @Component({ selector: 'app-test', template: ` @if(viewControl() === 'grid') { } @else if(viewControl() === 'card') { } `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, }) export class TestComponent { viewControl = signal<'grid' | 'card'>('grid'); onViewChange(view: 'grid' | 'card'): void { this.viewControl.update(() => view); } } ``` Is `signal()` the only viable option for this? Not really. An alternative approach could be to create a child component implementing `ControlValueAccessor` and manage state using reactive forms in the parent to control which view is displayed. ```TS @Component({ selector: 'app-test', template: ` @if(viewControl.value === 'grid') { } @else if(viewControl.value === 'card') { } `, imports: [ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, }) export class TestComponent { viewControl = new FormControl<'grid' | 'card'>('grid'); } ``` Creating a separate component for our buttons is a realistic approach, especially when dealing with multiple buttons and more complex logic. Worth mentioning that this example will not work with zoneless apps, because `viewControl.value` is not reactive. The framework will not know about the change. It might work in apps with zone.js, but only because of the "click" event, which is patched by zone.js. Instead of using reactive forms, we can improve this scenario by leveraging the [model() signal](https://angular.dev/api/core/model?ref=angularspace.com) in the child component. This allows the child to notify the parent about which button was clicked, effectively delegating the responsibility of toggling between `card` and `grid` views to the child component. Note that `viewModel` is a `viewModel = model<'grid' | 'card'>('grid')` in the child component. ```TS @Component({ selector: 'app-test', template: ` @if(viewControl() === 'grid') { } @else if(viewControl() === 'card') { } `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, }) export class TestComponent { viewControl = signal<'grid' | 'card'>('grid'); } ``` This is all well and good. We now have at least three (if not more) different ways to solve the same problem, and honestly, the differences between them aren’t that significant. Based on the examples, I’d lean toward the third solution. However, as a big RxJS fanboy, I want to dive deeper into `effect`, `httpResource`, `rxResource`, and `linkedSignal`, how they simplify code, yet still have (for now) some limitations when it comes to shaping the result value. ## Opinion On Effect When `effect()` was introduced alongside Angular signals, it's safe to say we all overused it. For a while, one of the most Googled questions was: > How to fix: Writing to signals is not allowed in a computed or an effect by default. Use allowSignalWrites …. We quickly discovered that creating infinite loops was easier than we’d like to admit. A classic example looks something like this: ```TS effect(() => { const user = authUser(); this.methodReadsAndUpdatedSignals(a); }) ``` Later, we discovered that we could use `untracked()` inside `effect()`, essentially wrapping most of our code in it, to prevent infinite loops. What’s interesting is that, despite `effect()` being part of the Angular signal API, Angular actually discourages its use except in [very rare cases](https://angular.dev/guide/signals?ref=angularspace.com#effects). When I built [ggfinance.io](https://ggfinance.io/?ref=angularspace.com) (*small promo* 😏), a mid-sized application, I found myself using `effect()` only in the following scenarios: - DOM – Initializing charts - DOM – Displaying data in Angular Material tables - DOM – Creating structural directives that modify the view based on signal (store) changes - LOG – Logging signal changes ```TS @Component({ selector: 'app-test', standalone: true, imports: [MatTableModule, MatPaginatorModule, MatSortModule], template: `
`, }) export class TestComponent { data = input(); paginator = viewChild(MatPaginator); sort = viewChild(MatSort); displayedColumns = ['col1', 'col2']; dataSource = new MatTableDataSource([]); tableEffect = effect(() => { const data = this.data(); untracked(() => { this.dataSource.data = data; this.dataSource.paginator = this.paginator() ?? null; this.dataSource.sort = this.sort() ?? null; this.dataSource._updateChangeSubscription(); }); }); } ``` Although `effect` is a building block as such, mainly to issue side-effect tasks, run an independent computation when one or more input values change, other alternatives are preferred when creating a new reactive data structure used in the DOM. Upon a later discovery, as I was writing this article, I realized that you solve some of the mentioned problems with `computed()`, such as rewriting the mat-table data population as such. ```TS @Component({ selector: 'app-test', standalone: true, imports: [MatTableModule, MatPaginatorModule, MatSortModule], template: `
`, }) export class TestComponent { data = input(); paginator = viewChild(MatPaginator); sort = viewChild(MatSort); displayedColumns = ['col1', 'col2']; tableData = computed(() => { const data = this.data(); const dataSource = new MatTableDataSource([]); dataSource.data = data; dataSource.paginator = this.paginator() ?? null; dataSource.sort = this.sort() ?? null; return dataSource }); } ``` ## Opinion On linkedSignal While `computed()` is a signal whose value is derived or computed from one or more other signals (dependencies), it is inherently a readonly signal. However, there are cases when you need to compute a signal, update its value over time, and then reset it back to its initial state when a dependency changes. An example of this might be in an online store, ordering application, or a simple SaaS app. Imagine you have a scenario where a user selects an item. The initial quantity will always be 1, but the user can increment the quantity. However, when a new item is selected, the quantity should reset to 1. Below are two examples of how to achieve this behavior, both before and after the introduction of `linkedSignal()`. ```TS // EXAMPLE BEFORE linkedSignal() itemSelected = signal(null); itemUnits = signal(1); // everytime a new item is selected, reset units to 0 itemUnitsEffect = effect(() => { const selected = this.itemSelected(); untracked(() => { this.itemUnits.set(1); }) }) ``` ```TS // EXAMPLE USING linkedSignal() itemSelected = signal(null); itemUnits = linkedSignal({ source: this.itemSelected, computation: () => 1 }); ``` I think `linkedSignal()` particularly useful in cases where: - State reset on dependency change, like in the example of selecting a product on an e-commerce site and resetting quantities back to the default value when a new item is selected. - Conditional state, when you want to link one signal’s state to another and have the ability to reset or adjust its value based on the context of the other signal. - Complex UI interactions, such as wizards or multi-step forms, where one step’s state depends on another, and you need to preserve or reset values depending on the flow. That said, `linkedSignal()` might not be necessary for every use case. It’s useful for scenarios where you need more than just a computed signal but don’t want to deal with the complexity of manually resetting or synchronizing state. ## Opinion On httpResource & rxResource/resource The recent Angular versions introduced two useful signal APIs designed to work with async data: - `httpResource` – [available in version 19.2](https://github.com/angular/angular/pull/59876?ref=angularspace.com) - `rxResource`/`resource` – [available in version 19.0](https://github.com/angular/angular/pull/58255?ref=angularspace.com) Initially, I had mixed feelings about these two functions because, since we can achieve very similar behavior using RxJS. However, I realized that the long-term goal of the Angular team seems to be reducing the reliance on RxJS. Looking at `httpResource`, its description says: > Uses HttpClient to make requests and supports interceptors, testing, and other features of the HttpClient API. Data is parsed as JSON by default. ```TS data1 = toSignal(this.http.get('...').pipe(map((d) => d.data) ), { initialValue: [] }); data2 = httpResource( () => ({ method: 'GET', url: '', }), { defaultValue: [], parse: (d): unknown[] => d.data }); ``` At first, I thought the two approaches were almost identical. However, I quickly realized I was mistaken. The `httpResource` API provides additional built-in states like `isLoading` and `error`, which are incredibly useful for tracking the request’s status. Furthermore, because `httpResource` is a `WritableResource`, it allows not just to observe state changes but also directly manipulate that state, manually setting the state of the signal, such as `data2.set([])`. This contrasts with the traditional `toSignal` API, which is a read-only signal. Additionally, `httpResource` gives us the `progress` of the HTTP request, and we can use `reload()` to trigger the HTTP request again. Let’s look at the following example. We have a search bar that loads items based on the prefix and selected genres, and when an item is selected, the search results will be dismissed. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/signal-types-personal-take-search-overview.png) Overview Of A Simple Search To achieve this behavior with the new signal APIs, we could implement something like the code below. I’d say the code is definitely easy to read and doesn’t require RxJS. As you type into the input, the values are saved in the `searchControl` signal. When the genre is changed, it gets saved in the `selectedGenresId` signal. I haven’t included the HTML part since I believe it's not crucial for this example. ```TS export class SearchComponent { private apiService = inject(AnimeApiService); selectedData = output(); // control signal to select a genre and item prefix searchControl = signal(''); selectedGenresId = signal(1); // load options if genre or prefix changes searchedDataResource = rxResource({ request: () => ({ genresId: this.selectedGenresId(), prefix: this.searchControl(), }), loader: ({ request }) => this.apiService.searchAnime(request.prefix, request.genresId), defaultValue: [], }); onGenresClick(id: number): void { this.selectedGenresId.set(id); } onClick(animeData: AnimeData): void { // emit to parent this.selectedData.emit(animeData) // reset displayed data this.searchedDataResource.value.set([]); } } ``` One small thing I’d like to highlight with this approach is the difference between [declarative and imperative programming](https://dev.to/krivanek06/from-chaos-to-clarity-simplify-your-angular-code-with-declarative-programming-58gm?ref=angularspace.com). In the `searchedDataResource`, we have the loaded items. However, when an item is selected, we imperatively change `searchedDataResource` to an empty array, somewhere in the program, to hide the loaded results. The question I want to explore is: What would be the RxJS equivalent of this code, where we want to keep track of both the loading and error states of network requests? ```TS export class SearchComponent { private apiService = inject(AnimeApiService); // control for item prefix searchControl = new FormControl('', { nonNullable: true, }); // control to select a genre selectedGenresIdControl = new FormControl(1, { nonNullable: true, }); selectedData$ = new Subject(); // notifies parent when item is selected selectedAnime = outputFromObservable(this.selectedAnime$); searchedData = toSignal( this.selectedGenresIdControl.valueChanges.pipe( switchMap((genderId) => this.searchControl.valueChanges.pipe( // search immediatelly with new genres startWith(this.searchControl.value), // load from API switchMap((name) => this.apiService.searchAnime(name, genderId).pipe( map((data) => ({ data, isLoading: false })), startWith({ data: [], isLoading: true }), catchError((e) => of({ data: [], error: e, isLoading: false }) ), ), ), // listen on select and reset the data switchMap((result) => this.selectedData$.pipe( map(() => ({ data: [], isLoading: false })), startWith(result), ), ), ), ), ), { initialValue: { data: [] as AnimeData[], isLoading: false } }, ); onClick(data: AnimeData): void { this.selectedData$.next(data); } } ``` One might argue that the `rxResource` version is shorter, more readable, therefore a better choice. While readability certainly wins here, what I personally miss from signals are the utility functions that RxJS provides, such as `distinctUntilChanged()`, `debounceTime()`, and others for manipulating data. To further reduce the complexity in RxJS, which typically handles the loading and error states of network requests, you can abstract the logic by [creating custom RxJS operators](https://dev.to/this-is-angular/create-custom-rxjs-operators-edd?ref=angularspace.com). If you want to minimize RxJS usage, but still need functionality like debouncing, you could either use [Lodash](https://lodash.com/docs/4.17.15?ref=angularspace.com) or combine RxJS with `rxResource` to achieve something like this: ```TS export class AnimeSearchNewComponent { private readonly apiService = inject(AnimeApiService); // control signal to select a genre and item prefix readonly searchControl = signal(''); readonly searchControlSignal = toSignal( toObservable(this.searchControl).pipe( distinctUntilChanged(), debounceTime(300), ) ) // load options if genre or prefix changes readonly searchedDataResource = rxResource({ request: () => ({ prefix: this.searchControlUsed(), }), loader: ({ request }) => this.apiService.searchAnime(request.prefix), defaultValue: [], }); } ``` One thing to consider with signals is that while they provide a simpler API for managing state, RxJS still has its place for more complex scenarios where you need to do heavy data transformation or work with [higher-order observables](https://dev.to/krivanek06/angular-interview-what-is-higher-order-observable-2k03?ref=angularspace.com). Signals shine in cases where you just need to track a value and its changes, whereas RxJS excels when you're dealing with multiple asynchronous streams, operators like `combineLatest` or `switchMap`, and complex transformations. ## Summary No one can argue that Angular's team doesn't deliver new and useful features. Perhaps it’s because I’ve been part of the ecosystem for so long, or because of my experience with RxJS, that I feel the need to discuss this topic. It’s also worth mentioning the new RFCs about resource architecture: - [Resource RFC 1: Architecture](https://github.com/angular/angular/discussions/60120?ref=angularspace.com) - [Resource RFC 2: APIs](https://github.com/angular/angular/discussions/60121?ref=angularspace.com) The ongoing RFCs about the resource architecture will likely shape the direction Angular takes with handling asynchronous data. When I first set out to write this article, I didn’t quite understand the usefulness of `rxResource` and `httpResource`, thinking that you could achieve the same or similar results with signals. However, after creating a small example of the search box – [available on GitHub](https://github.com/krivanek06/stackblitz-signal-example?ref=angularspace.com) – I came to appreciate that having more options to accomplish the same result is actually a good thing. Whether you choose RxJS or signals depends on your preferences and the needs of your project as both have their place in Angular development. Finally I do recommend taking a look on [HttpResource in Angular 19.2 from Decoded Frontend](https://www.youtube.com/watch?v=GXzPlaF3-bE&ref=angularspace.com) as he explains lots of functionalities with the `HttpResource`. I hope you enjoyed my musings on this topic. You can also read more from me on [dev.to](https://dev.to/krivanek06?ref=angularspace.com) or connect with me on [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/Screenshot-2024-11-13-at-15.52.00--1---3---1---1---1-_upscayl_2x_high-fidelity-4x.png) ### Senior Engineer to Lead - Full Scholarship 1x Giveaway URL: https://www.angularspace.com/senior-engineer-to-lead-full-scholarship-1x-giveaway/ Last updated: 2025-03-06T16:33:59.000Z Today I have a very special giveaway! _This post is for subscribers only._ ### Underrated Angular Features URL: https://www.angularspace.com/underrated-angular-features/ Last updated: 2025-03-04T17:43:28.000Z Angular is a very, *very* powerful framework, which means it has lots of features. Of course, everyone knows the essentials, like components, services, routing, etc. But obviously, almost every Angular developer has some features they've never tried, or even heard of. In today's article, we are going to cover some of the features that are relatively obscure, but can supercharge our applications. Let's get started! ## Using more complex directive selectors Every Angular developer has, at some point, authored a custom directive. In my experience, in 95% of the cases, the directive selector is just a simple attribute selector. But Angular actually allows us to use more complex selectors, like class selectors, element selectors, and so on. ### How can this be useful? Let's cover one example to see how this can be useful. Let's say in our application we have a rule that long texts get truncated, with a trailing ellipsis (...) at the end. For this purpose, we created a `.truncate` CSS class that we can just drop on any element to make it truncated: ```css .truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ``` This works fine, and now we have thousands of HTML elements in our templates that look like this: ```html

This is a long text that will be truncated

``` However, a new requirement just dropped, and now we need to add a tooltip to the truncated text, so that when the user hovers over the element, they can see the full content. Let's assume we already have a `TooltipDirective` that we can use to add tooltips to elements. So, now, what is left to do is go through hundreds of files and add the directive to each element... wait, this doesn't sound very exciting. Thankfully, there is a solution to this! We can write a directive that will bind to the `.truncate` class and add the `TooltipDirective` to the element as a Host Directive. Here is how we can do this: ```typescript @Directive({ selector: '.truncate', hostDirectives: [TooltipDirective], host: { '(mouseenter)': 'showTooltip()', '(mouseleave)': 'hideTooltip()' } }) export class TruncateDirective { readonly #elRef = inject(ElementRef); readonly #tooltipRef = inject(TooltipDirective); showTooltip() { this.#tooltipRef.show(this.#elRef.nativeElement.textContent); } hideTooltip() { this.#tooltipRef.hide(); } } ``` Now, we can simply import the directive in any relevant component and all elements with the `.truncate` class will automatically have the tooltip functionality! Another great use case is utilizing the `:not()` selector. This can come handy when we add some boolean input to prevent a directive from executing. For example, in this directive, we might have scenarios where the UI is not suitable to display the tooltip, so we can add a `noTooltip` input to the directive: ```typescript @Directive({ selector: '.truncate', hostDirectives: [TooltipDirective], host: { '(mouseenter)': 'showTooltip()', '(mouseleave)': 'hideTooltip()' } }) export class TruncateDirective { noTooltip = input(false); readonly #elRef = inject(ElementRef); readonly #tooltipRef = inject(TooltipDirective); showTooltip() { if (!this.noTooltip) { this.#tooltipRef.show(this.#elRef.nativeElement.textContent); } } hideTooltip() { this.#tooltipRef.hide(); } } ``` However, this complicates the logic of this directive, and it still gets applied to any elements with class `.truncate` (and the `TooltipDirective` as a host directive with it). But thankfully, we can just prevent the directive being applied at all by excluding elements with a specific attribute. Here is how we can do this: ```typescript @Directive({ selector: '.truncate:not([noTooltip])', hostDirectives: [TooltipDirective], host: { '(mouseenter)': 'showTooltip()', '(mouseleave)': 'hideTooltip()' } }) export class TruncateDirective { @Input() noTooltip = false; readonly #elRef = inject(ElementRef); readonly #tooltipRef = inject(TooltipDirective); showTooltip() { this.#tooltipRef.show(this.#elRef.nativeElement.textContent); } hideTooltip() { this.#tooltipRef.hide(); } } ``` Then, we can just use the directive like this: ```html

This is a long text that will be truncated with a tooltip

This is a long text that will be truncated without a tooltip

``` As a side note, directives are actually extremely powerful, and if you haven't, I'd suggest reading my mega-article on directives [here](https://www.angularspace.com/mega-article-superpowers-with-directives-and-dependency-injection/). Now, let's move to a really obscure feature. ## Reading a service from a view child While using view children is not very common, every Angular developer has done it at least once in their career. As we know, view children are used to access child components, directives, or elements in the template. If our knowledge is a bit more advanced, we might also know that we can read the native element of a view child, or its view container reference (using the `{read: ViewContainerRef}` option or the `{read: ElementRef}` option). However, what is truly fascinating, we can also read an instance of a service provided on a view child! Now, there are to ways of achieving it. One is to specify which component provides the service, and read it from that specific view child: ```typescript @Component({ selector: 'app-parent', template: ` ` }) export class ParentComponent { myService = viewChild(ChildComponent, { read: MyService }); someMethod() { // Now we can call the service method this.myService().doSomething(); } } ``` > Note that we call the `myService`, as `viewChild` return a signal, so, in this case, it will be a signal of the service we requested. The second way is to read the service from any view child, regardless of which component provides it. This can be done by actually just querying the service directly: ```typescript @Component({ selector: 'app-parent', template: ` ` }) export class ParentComponent { myService = viewChild(MyService); someMethod() { // Now we can call the service method this.myService().doSomething(); } } ``` In this case, however, we can't be sure which instance of the service we are getting, as the view might contain several child components that provide this service. This query will retrieve the first instance. ## How can this be useful? While the feature itself might be quite exotic, some useful cases can be found for it. Imagine we have a store service that contains data related to some child components, for example, different types of tasks on some columns on a task board. Each child component provides its own version of the store so that its data is fully separated form neighboring columns. > Warning: this approach is, in general, an anti-pattern, as it is breaking the usual data flow approach (directly modifying the state in the child component might result in UI changes in the parent), and should be used only in scenarios where we do not own the child components (for instance, they come from a third-party library or from another team over which we do not have control). In general, it is better to rely on inputs/outputs or a centralized data store. Use this with caution only when *absolutely* necessary However, in the parent component, we wish to display the total number of tasks. We can achieve this easily with `viewChildren` and computed signals: ```typescript @Component({ selector: 'app-parent', template: `

Total tasks: {{ total() }}

` }) export class TaskBoard { taskStores = viewChildren(TaskStore); // query all instances of the task store in the view // get the total of each task store and add them to get the total number of tasks total = computed(() => this.taskStores().reduce((acc, store) => acc + store.tasks().length, 0)); } ``` This is worth remembering when dealing with complex scenarios where we don't want to modify the child components and instead want to try to move all the logic to the parent component. Now, let us take a look at a relatively more known, but still underutilized feature. # Content projection with named slots We all used some content projection in Angular, and might event implemented our own components that use `ng-content`. However, what many developers don't use quite often is the ability to control where and what content should be projected. This can be done by using named slots, and might helps us decrease the complexity of our components and also maybe reduce the number of configuration inputs it receives. Imagine we are building our own custom rich text editor. It is going to be used in a lot of places across the application, and almost every time it supports a different set of features. For example, our component might support copy-paste buttons. undo-redo-buttons, and text formatting buttons (like bold text, italics, etc). We diligently created separate components for all of those buttons and now our component looks like this: ```typescript @Component({ selector: 'app-rich-text-editor', template: `
@if (undoRedo()) { } @if (copyPaste()) { } @if (textFormatting()) { }
` }) export class RichTextEditor { undoRedo = input(false); copyPaste = input(false); textFormatting = input(false); } ``` No with this, we can customize each place where the rich text editor is used, getting a pretty decent developer experience. However, this approach has a couple of downsides. 1. The number of component inputs will only grow as we add more features to the editor. Take a look at this: ```html ``` This just doesn't look very nice 2\. The component code itself will be riddled with `@if` statements, which isn't exactly amazing for readability 3\. Deteriorated tree-shaking: for example, the users might only visit a page where they can see only the simplest version of the text editor, but all the code for the other features will still be included in the bundle. So, instead, we can use named slots to project the content where we want it. Here is how we can do it: ```typescript @Component({ selector: 'app-rich-text-editor', template: `
` }) export class RichTextEditor {} ``` As we can see, we removed all the `@if` statements and all the inputs for the rich text editor. Now, we can use the component like this: ```html ``` ### How is this useful? Now, as we can see, we do not have to write a bunch of inputs to use the component, we can just drop the buttons we need in the editor. The component code itself has also improved, while using named slots ensured that *a)* the components are inserted at their correct place and *b)* no other custom templates can be inserted into the editor template. In addition to it, we have great tree-shaking, as each component that uses the rich text editor will itself decide which nested components will be included in its bundle. Now, next let us see a feature that has a bit of a limited scope, but can be extremely useful in some scenarios. # Using Shadow DOM For this feature, we can continue the previous example with the rich text editor. If we are using some third-party editor, it usually will export some custom styles HTML based on what the user has typed and formatted in their text. Then, we would want to display that HTML in some other place in our application using `innerHTML`: ```typescript @Component({ selector: 'app-custom-text', template: `
` }) export class CustomText { readonly #sanitizer = inject(DomSanitizer); rawHtml = input.required(); // here we use the sanitizer to inform Angular the HTML is safe // be very careful with this, as it can lead to XSS attacks // if you are unsure about the input, or have not sanitized it in any way // DO NOT use it directly! html = computed(() => this.#sanitizer.bypassSecurityTrustHtml(this.rawHtml())); } ``` This works, however, it can result in a funny problem: the styles from the rich text editor will be applied to the whole application, not just the `app-custom-text` component. This is because the styles are not encapsulated in the component, and are applied globally. At some point all of us heard about CSS encapsulation, and in general we think about it this way: Angular components isolate their views from other components, so if component A has some styles applied to the CSS class `.my-class`, they will not affect a `
` in component B template. This is commonly know as `viewEncapsulation`. Byt default, Angular uses `Emulated` view encapsulation, which means it will emulate the shadow DOM, adding some randomly generated attributes to component templates to ensure styles from different components do not clash. This, however, opens the pathway to modify CSS from the global scope - this is why CSS styles written in the `styles.css` file will be applied to the whole application. The same issue happens with the `innerHTML` property, as it will apply the styles globally, without the randomly generated attributes. To avoid this, we can make the component's style truly encapsulated by using the `ShadowDOM` option: ```typescript @Component({ selector: 'app-custom-text', template: `
`, encapsulation: ViewEncapsulation.ShadowDom }) export class CustomText { readonly #sanitizer = inject(DomSanitizer); rawHtml = input.required(); html = computed(() => this.#sanitizer.bypassSecurityTrustHtml(this.rawHtml())); } ``` This way, we can be sure anything the user formats in their custom text won't affect hot the rest of the application looks. Read more about the Shadow DOM [here](https://developer.mozilla.org/en-US/docs/Web/API/Web%5Fcomponents/Using%5Fshadow%5FDOM?ref=angularspace.com). # Reusing existing providers To better understand this feature, let's imagine a scenario: a developer before us has authored a reusable component that receives a user ID and renders a `UserProfileComponent`. But there is a catch - our app has two types of users, our own users and also third-party users with some limited access to features. For this purpose, we have two services, `UserService` and `ThirdPartyUserService`, both provided at the application root, that work with corresponding APIs to ensure we get the correct data. However, the `UserProfileComponent` only works with the `UserService`, as it directly injects `UserService` in its constructor. We could modify the component to accept a service as an input, or maybe perform some other trickery, but this might result in other breaking changes and increased verbosity, which we would love to avoid. Thankfully, the `UserService` and `ThirdPartyUserService` both implement the same interface, having the same methods with just the API endpoints themselves being different. This means, we can utilize an underrated feature of Angular - the `useExisting` provider option, to ensure the pages that work with third party users make the `UserProfileComponent` work with the `ThirdPartyUserService` under the hood: ```typescript @Component({ selector: 'third-party-component' template: ` `, providers: [ { provide: UserService, useExisting: ThirdPartyUserService } ] }) export class ThirdPartyComponent { userId = input.required(); } ``` So, what happens here? When the `UserProfileComponent` injects the `UserService`, in other cases, it will get the instance of `UserService` from the root, but in our component, it will stumble upon this `useExisting` provider which will give it the instance of `ThirdPartyUserService` (again from the root - we are reusing the `ThirdPartyUserService`, not creating a new instance). This ensures that the `UserProfileComponent` works with the correct API here, but that also there won't be any new instances of that API service, potentially breaking the shared data between the components that use it. ## `NgZone.runOutsideAngular` Imagine we are implementing a "Scroll to top" button in our application. We want a reusable component that would listen to user's scroll events and show/hide the button based on the scroll position. We could implement it like this: ```typescript @Component({ selector: 'app-scroll-to-top', template: ` @if (showButton()) { } ` }) export class ScrollToTop { showButton = toSignal( fromEvent(window, 'scroll').pipe( map(() => window.scrollY > 100) ), ); } ``` And this will work properly, however, there is a major issue with this implementation in terms of performance. Actually, it triggers **a lot** of change detection cycles. We can easily check this by adding a method to the template that will log something on each change detection cycle: ```typescript @Component({ selector: 'app-scroll-to-top', template: ` @if (showButton()) { } {{ logChangeDetection() }} ` }) export class ScrollToTop { showButton = toSignal( fromEvent(window, 'scroll').pipe( map(() => window.scrollY > 100) ), ); logChangeDetection() { console.log('Change detection triggered'); } } ``` We'll quickly see that the number of unnecessary CD cycles is simple unacceptable. So, why is this happening? In short, Angular uses the Zone.js library to monitor asynchronous event listeners and trigger change detection every time such an even happens to check if these async operations resulted in any changes to template bindings, and if so, update the view. While this is obviously useful, it's very easy to accidentally trigger an avalanche of CD cycles, as we have seen here. With this component, we subscribe to the `scroll` event, which obviously happens a lot, triggering unnecessary CD cycles. So, what can we do about it? We can utilize the `NgZone` injectable to inform Zone.js not to track this particular event listener: ```typescript @Component({ selector: 'app-scroll-to-top', template: ` @if (showButton()) { } {{ logChangeDetection() }} ` }) export class ScrollToTop { showButton = signal(false); constructor(private readonly #zone: NgZone) { this.#zone.runOutsideAngular(() => { fromEvent(window, 'scroll').subscribe(() => { this.showButton.set(window.scrollY > 100); }); }); } logChangeDetection() { console.log('Change detection triggered'); } } ``` Now, if we re-check the console output, we'll see that the number of CD cycles has decreased significantly. This is because we are now telling Zone.js to ignore the `scroll` event listener, and instead rely on the `showButton` signal itself to perform change detection. Signal updates always trigger change detection, but we are safe from having too many cycles because CD will only be triggered when the `showButton` signal actually changes its value (so setting `false` on it while it's already `false` won't trigger CD). As we can see, this is a very easy way to significantly boost our application's performance. > Note: in the future, Angular application will be Zoneless (i.e. not rely on Zone.js for change detection). However, considering the overwhelming majority of existing Angular applications use Zone.js as of 2025, feel free to use `NgZone.runOutsideAngular` to improve performance in your applications, but try to avoid other `NgZone` methods like `onStable`, `onMicrotaskEmpty`, etc. as they might not work as expected when the app turns Zoneless in the future. ## Conclusion Angular has a lot of features, quite a lot of which are not very well-known; I cannot hope to cover them all in one article. However, with this one, I hope to help developers discover some of the more obscure features that can be extremely useful in some scenarios. ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/Screenshot-2024-11-13-at-15.jpg) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/03/angular-university-banner-4--1-.jpg)](https://angular-university.io/?ref=angularspace.com) ### FREE WEEKEND & Angular Certificate GIVEAWAY URL: https://www.angularspace.com/free-weekend-angular-certificate-giveaway/ Last updated: 2025-02-22T08:14:46.000Z Something special is cooking, and it's going to be a tasty meal for your brain! Get ready to be served over a weekend with completely FREE access to the _This post is for subscribers only._ ### Symptoms of an Angular Disorder URL: https://www.angularspace.com/symptoms-of-an-angular-disorder/ Last updated: 2025-02-19T15:25:13.000Z There are millions of Angular projects out there, and we have undoubtedly encountered lots of poorly written code. Also, there are many "Angular Bad Practices" articles—I wrote several of them—but trust me, this is not one of them. Bad practices often involve phrases like "avoid too complex components" or "separate logic," which sound quite abstract. This often means that we might answer interview questions about these bad practices but will have a hard time recognizing them in the codebase. So, in this article, we are going to tackle some of the symptoms that might not themselves be considered bad practices but can be indicative of a larger chronic illness that a given Angular project might be suffering from. > Note: it is important to understand that everything mentioned in this article is only an indication that something might be off, and we do not seek to refactor everything just to avoid some instances of these "symptoms". Let's get started! ## Too many inputs on a components Sometimes, we might see a component that tries to be *incredibly* customizable. This is not necessarily a bad thing itself, however, it often results in a very cumbersome component. Take a look at this component that is displaying a dropdown: ```html ``` Now, I have purposefully not provided the code for the component itself, but you can imagine what mess is going on there: a lot of `if` statements to handle the dropdown being multiselect or not, a lot of mess in the template, long code just to define inputs. ### What is the problem? 1. Huge component that is hard to maintain 2. Ugly template whenever we use this component 3. Easy to forget a property unless marked as required ### What can be done? 1. Break the component into two: dropdown and multiselect 2. Use a configuration object instead of multiple inputs 3. Injectable provider of default options so they can be customized This can result in a much better template: ```html ``` And, alternatively, we can use a default configuration: ```typescript const appConfig: ApplicationConfig = { providers: [ { provide: MultiselectConfig, useValue: { searchable: true, virtualScrolling: false, clearable: true } } ], }; ``` Then, we won't even need to provide the `config` input unless we want to override the defaults. Also, the component can implement a `mergeConfig` functionality so that only configs provided via the input override the default configuration. Also, if the default configuration needs to be different for a certain part of the application, it is easy to provide a different configuration for only that parts using providers in routing: ```typescript const routes: Routes = [ { path: 'feature', loadChildren: () => import('./feature/feature.routes'), providers: [ { provide: MultiselectConfig, useValue: { searchable: false, virtualScrolling: true, clearable: false } } ] }, ]; ``` As we can see, components being flexible is not bad, however, trying to be over-the-top generic can result in lots of trouble, and it might be best for all parties involved to break the components into different ones depending on their purpose and use configurations objects + Dependency Injection (DI) to simplify everything. ### When can this be permissible? In the case of large, highly customizable UI component libraries, having a lot of inputs can make sense. However, both pieces of advice mentioned above still apply - it might be a good idea to use them. Now, onto the next one! ## Injecting a parent component Sometimes, when we work with complex UIs that we already broke down into smaller components, we might encounter situations where normal ways of communicating between those aren't enough. For instance, almost everyone was tempted at some point to pass a `formControl` or `formGroup` to a child component as an input. While this can work in a simple scenario, it will introduce more complexity and even bugs when an `OnPush` change detection strategy is applied to the child component (parent sets the value of a control but that is not readily apparent in the child component if it uses `OnPush`). So, in such scenarios, developers sometimes (thought thankfully not very often) come up with a the idea of just injecting the parent component and using its reference to access anything it has. Here is the simples showcase: ```typescript @Component({ selector: 'app-child', template: `{{ parentComponent.someProperty }}` }) export class ChildComponent { parentComponent = inject(ParentComponent); } ``` ### What is the problem? 1. Incredibly tight coupling between the two components - Child components simply cannot function without being nested in the parent component's template (this can be circumvented by using the `optional` DI lookup modifier, but will introduce even more complexity) 2. Hard to test - now we need to mock the parent component in the child component's tests 3. Can still results in change detection issues 4. Bugs related to data flow will become very hard to find and fix ### What can be done? 1. Avoid doing this at all costs 2. Use injectables (services, `InjectionToken`\-s) to share data that cannot be shared via inputs Here is an example of moving the `formGroup` to an injectable: ```typescript export const CustomFormGroup = new InjectionToken('CustomFormGroup', { factory: () => new FormGroup({ // whatever controls go here }), }); @Component({ selector: 'app-child', }) export class ChildComponent { form = inject(CustomFormGroup); } @Component({ selector: 'app-parent', template: ``, }) export class ParentComponent { form = inject(CustomFormGroup); } ``` As we can see, both components now have access to the same `formGroup`, but are not tightly coupled. Additionally, if we are talking about a small form, using [ControlValueAccessor](https://angular.dev/api/forms/ControlValueAccessor?ref=angularspace.com) might also be a good idea. ### When can this be permissible? When we are building components that are actually meant to only be used together, this can make sense. For example, a `TabsComponent` might have projected `TabComponent`\-s inside its template: ```html Content 1 Content 2 ``` In this case, the `TabComponent` does not make sense on its own, and it will be perfectly okay to inject the `TabsComponent` into it. Now, let's talk about an elusive case. ## Large *for* loops in template We often talk about how we need to break components down into smaller ones, but it is often not very obvious when this "threshold" has been crossed and we need to create a new child component and move some logic over there. However, one big indication for this is having a `*ngFor` or `@for` loop in the template with a large template inside. Consider the following snippet: ```html @for (comment of comments(); track comment.id) {
{{ comment.author.name }}
{{ comment.body }}
} ``` ### What is the problem? 1. Large piece of UI that can easily be abstracted away 2. A separate cognitive unit that "breaks" the logical flow of the UI: we can think of "one comment" and a "list of comments" with the understanding that it might have the functionality from above. 3. Less code in a given template ### What can be done? Now this one is very obvious, just move the piece of this template to a child component: ```html @for (comment of comments(); track comment.id) { /> } ``` ### When can this be permissible? When we are dealing with a very simple piece of HTML code, abstracting away might just complicate things: ```html @for (tag of tags(); track tag.id) {
{{ item }}
} ``` No need to move this simple piece of code to a separate component. But now, let's move to the child component itself and discuss some red flags there ## Service injected in a supposedly presentational component There is a lots of talk about the container-presenter pattern, and how we should separate logic from the UI in all of front-end framework communities. In short, the idea is that we should have components that are responsible for the UI only, which would receive the data they need via inputs (and optionally emit events via outputs), and components that are responsible for the logic only, like getting the necessary data, setting up forms, etc. For instance, the very `` component we discussed above is an example of such a presentational component: it receives the comment data and emits events when a user interacts with it for the parent component to handle. Such a component is good when it is only used to *display* the data; and why is that? Well, imagine we have a lot of different section in our apps that have comments: a product description that allows user reviews, a blog post that allows comments, maybe videos, and so on. Eash time the comment looks exactly the same, however underneath, it might perform differently: a comment in a video section wants to update a completely different entity with a completely different URL (like "videos//comments/") than, say, a comment in the blog post section ("posts//comments/"). With such a situation, it makes sense that we might want to make the `CommentComponent` as simple and reusable as possible, and delegate the logic of updating the comment to the parent component. Now, imagine we did something like this inside the `CommentComponent`: ```typescript @Component({ selector: 'app-comment', template: `...` }) export class CommentComponent { readonly #commentService = inject(CommentService); } ``` ### What is the problem? 1. The component is no longer independent and less reusable - we can only use it in contexts where the `CommentService` is available 2. The component does way more than one might expect from a component that is supposed to just display some UI 3. Harder to test - now we need to mock the service in the component's tests ### What can be done? There are no easy solutions to this since when the service is injected into a component, it most probably is being used somewhere, creating tight couplings. The way to deal with this is to delegate the functionality to the parent component. ### When can this be permissible? When we are building a component that is actually meant to be used in a very specific context, this can make sense. For instance, a `UserComponent` that is supposed to display the user's data and allow them to change their password might have a `UserService` injected into it. However, in most cases, this is a red flag that the component is doing too much and should be refactored. ## Using manual change detection Now, this one is tricky, and happens rarely, but you might run into some code like this: ```typescript @Component({ selector: 'app-comment', template: `...` }) export class SomeComponent { readonly #cdr = inject(ChangeDetectorRef); someMethod() { someObservable$.subscribe(() => { this.#cdr.detectChanges(); }); } } ``` Now, it is not necessary that the code would contain an Observable, but it is often the case. So, what does it even mean? Well, Angular has a very powerful change detection mechanism that is able to detect changes in the component's template bindings and update the UI accordingly. As we all know, this process is fully automatic and very, very rarely needs manual enhancement. ### What is the problem? 1. A bit confusing for another developer who reads this code - why is the change detection being called manually? 2. Might actually be unnecessary - change detection is a complex topic and sometimes developers, having not understood it properly, encounter a complex scenario and put a manual call to change detection "just in case" 3. Increases the amount of CD cycles, which might potentially result in performance issues ### What can be done? 1. Do not use manual change detection unless you are absolutely sure you need it 2. `async` pipe in template, in case when you deal with observables 3. Setter in @Input (if you work with versions prior 16) or a combination of input + computed from a signal 4. Update your state from sync functions executed from template (event) 5. Using pipes to transform data 6. If you feel you need it, examine the code as a whole and try to figure out why it works the way it works. Switching to Observables/signals might help improve the situation 7. If absolutely necessary, at least provide a detailed comment explaining why the manual change detection is being used in a given situation ### When can this be permissible? It is hard to predict when this might be necessary. One scenario can be if we're developing a complex component that also works with a third-party library that does not play well with Angular's change detection. Maybe it registers a lot of listeners and results in lots of unnecessary CD cycles. In such cases, we might opt-in into running the component outside of Angular's zone (using `ngZone.runOutsideAngular()`), and then manually call change detection when necessary. ## Lots of `subscribe` calls Now, this is a popular one: we have all seen a dreaded component like this: ```typescript @Component({...}) export class SomeComponent implements OnInit { readonly #dataService = inject(DataService); form = new FormGroup({ name: new FormControl(''), email: new FormControl(''), }); ngOnInit() { this.#dataService.getData().subscribe(data => { this.form.patchValue(data); }); this.form.valueChanges.subscribe(value => { this.#dataService.updateData(value); }); this.form.statusChanges.subscribe(status => { if (status === 'VALID') { this.#dataService.saveData(this.form.value); } }); } } ``` As we can see, this component does a lot in terms of subscribing to things. And we didn't even include logic to unsubscribe from these Observables! ### What is the problem? 1. Hard to maintain - we need to keep track of all subscriptions and unsubscribe from them when the component is destroyed 2. Potential race conditions - sometimes some data from Observable A is needed in the subscription to Observable B, but arrives later than B, causing bugs and requiring even more complexity 3. Looks bad - this one is not even subjective, the code it really hard to read and visually "scary" 4. Has the potential to grow even worse ### What can be done? 1. Every time an Observable is used to extract data to a local property, (`someObservable$.subscribe(data => this.data = data)` scenario), ditch that subscription and use the `async` pipe in the template, or, alternatively, convert the Observable to a signal 2. When subscribing a lot to some Reactive Forms controls, consider switching the form to a template-driven form with signals and utilize tools like `computed`, `linkedSignal`, and `effect`. Read more about this in my [blog post](https://armenvardanyan.dev/blog/why-i-switched-to-template-driven-forms?ref=angularspace.com). 3. If subscriptions are necessary to handle side-effects of HTTP calls (loading, error), consider using the [Resource API](https://www.angularspace.com/everything-you-need-to-know-abour-resource-for-now/). ### When can this be permissible? When dealing with a third-party API that works in an imperative way (like the reactive forms themselves), there might be no other option than to subscribe. Try your best to minimize the amount of subscriptions and only resort to them when absolutely confident there is no other way. ## Zero or very few custom directives Finally, this one is more of a general advice, but if a codebase lacks any custom directives (or has very few of them), it might be a sign that the developers are not utilizing the full power of Angular. ### What is the problem? 1. A fairly large application will definitely have some complex template logic that might be better off being in a directive, but probably gets copy-pasted around 2. Sometimes components are used to handle some UI logic that can be done via a directive instead, complicating the template code 3. This might also signal that developers are not very familiar with all the powers of Angular ### What can be done? There is no general solution to this, but the very first step would be learning about the powers of Angular directives. I would suggest reading my mega-article about [Angular Directives](https://www.angularspace.com/mega-article-superpowers-with-directives-and-dependency-injection/), which covers *everything* you need to know about them. Next, you might start noticing patterns in your template that repeat a lot, and think about how they can be turned into a directive. For instance, if you have a lot of elements that need to be shown only to authenticated users, you might create an `*appAuthenticated` directive that would handle this logic. Here is an example of such a directive: ```typescript @Directive({ selector: '[appAuthenticated]' }) export class AuthenticatedDirective implements AfterViewInit { readonly #templateRef = inject(TemplateRef); readonly #viewContainerRef = inject(ViewContainerRef); readonly #authService = inject(AuthService); ngAfterViewInit() { if (this.#authService.isAuthenticated()) { this.#viewContainerRef.createEmbeddedView(this.#templateRef); } } } ``` And then, you can simple use it in your template whenever you want to hide elements based on user's auth status: ```html
``` Finally, you might notice components that don't actually modify the UI a lot and rather act as "add-ons" to some existing elements, and consider if you can actually use a directive in their stead, simplifying your template ### When can this be permissible? When you are working on a very small project that does not have a lot of complex UI logic, it might be okay to not have any custom directives. However, as the project grows, pay attention tou your templates and components, and some patterns will probably emerge ## In Conclusion Angular is a huge framework with lots of interconnected parts, and there are a bunch of good and bad practices, of which we all are aware to some degree. As sometimes it is hard to recognize a bad pattern, I am hopeful this article will shed some light on how we can start thinking about our Angular codebases to reveal underlying issues and quickly fix them. ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/02/Screenshot-2024-11-13-at-15.52.00--1---3---1---1-.jpg) ### Simple User Event Tracker In Angular URL: https://www.angularspace.com/simple-user-event-tracker-in-angular/ Last updated: 2025-02-17T15:14:08.000Z Recently, in the company I am working for, we had a task to implement an user event logger. The problem was that we weren’t sure how the customer is using our app. We needed to track user behavior to understand why certain HTTP actions were being triggered. The task was a bit abstract, but we had to fulfill the following requirements: - Log every single input value change - Track every form submission, regardless of validity - Capture every router navigation change - Monitor dialog open and close events - Log all incoming and outgoing HTTP requests - Aggregate logs into chunks to optimize storage - Save logs when the application is refreshed/closed While there are many 3rd party providers, or even NPM libraries to choose from, we wanted to have a local event tracker that can be customized based on our needs. In this article, I’ll go through a real world problem, the different approaches we considered, and the final solution we implemented. The logger is quite generic, hence the reason for this article. If you have a similar problem, you may find some inspiration here. The code is available on [Github Repository](https://github.com/krivanek06/stackblitz-angular-user-event-logging?ref=angularspace.com). The article will be rich with code snippets, but I won’t explain every part of the code, as I assume the reader have a basic knowledge of declarative programming, RxJs operators and Angular’s fundamentals. ## The End Result Before diving deep into the article I want to show a picture about the final result. As users interact with the application, changing form values, opening dialogs, submitting forms, clicking buttons, and more, every action is logged. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/02/article-logs-1.png) Overview Of The Generated Logs ## Initial Brainstorming When brainstorming a solution for this problem, to handle HTTP requests, the answer was obvious, we needed to create an interceptor. The problem was to track input value changes, which included: text fields, text areas, selects, radio buttons, button clicks, etc. We considered two approaches: 1. **Multiple Directives** – Creating separate directives for different HTML elements, such as `@Directive({ selector: 'input, textarea' })`. 2. **A Centralized Service** – Injecting the `DOCUMENT` token (`document = inject(DOCUMENT)`) into a service, setting up a global event listener, and identifying which HTML element was clicked or focused. While directives are great for targeting specific elements, a major drawback (for us) was that each directive had to be explicitly imported into every standalone component. To keep things simple, we went for the "one service to rule them all" approach, though we still used directives in some edge cases. In the following sections, I'll walk you through our solution. ## Event Accumulator To start with the basics, we first needed a service to collect and store all generated logs before sending them to the server when the application closed. This part of the solution was relatively simple, and we ended up with the following approach: ```TS @Injectable({ providedIn: 'root', }) export class UserEventTrackerService { private readonly router = inject(Router); /** trigger when an user event happens that we want to log */ private readonly accumulateLog$ = new Subject(); /** trigger to reset the accumulated logs */ private readonly resetLogs$ = new Subject(); /** accumulate every user event that happens */ private readonly accumulatedLogs = toSignal( merge( // triggered logs by the app this.accumulateLog$.pipe( map((action) => ({ type: 'add' as const, action: { ...action, time: new Date(), page: this.router.url }, })), ), // reset logs this.resetLogs$.pipe(map(() => ({ type: 'reset' as const }))), ).pipe(scan((acc, curr) => (curr.type === 'add' ? [...acc, curr.action] : []) , [] as UserEvent[]) ), { initialValue: [] }, ); createLog(action: LogEventAction): void { this.accumulateLog$.next(action); } saveLogs(): void { const logChunks = createChunks(this.accumulatedLogs(), 120) // save all log chunks for (const logFormat of logChunks) { this.sendToRemoteByFetch(logFormat); } // trigger reseting all previous logs this.resetLogs$.next(); } private sendToRemote(body: unknown): void { // todo .... } } ``` In the `UserEventTrackerService`, we expose two public interfaces. When we want to create a new log, we call the `createLog()` method. Not exposing the `accumulateLog$` subject, but rather wrapping it inside the `createLog()` method prevents unnecessary abuse such as calling `accumulateLog$.complete()` for whatever reason. The `LogEventAction` type helps categorize and distinguish different types of logs. While not the main focus of this article, for context, the `LogEventAction` looks something like this: ```TS // the code is reduced for the sake of article export type LogEventAction = | { type: 'inputChange'; // input, select, checkbox elementType: string; // label of the element elementLabel: string; // input value value: string | boolean | number; } | { type: 'clickElement'; elementType: string; value: string; } | { type: 'routerChange'; text: string; } | { type: 'apiCall'; url: string; } | { type: 'custom'; value: unknown; } // ..... export type UserEvent = { // time when the user event happens - HH:mm:ss time: string; // current page that the user is on page: string; } & LogEventAction; ``` Therefore, if we want to create a log, all we have to do in a component is: ```TS export class TestComponent { private trackingService = inject(UserEventTrackerService); createLog(){ this.trackingService.createLog({ type: 'custom', value: 'AA' }) } } ``` The other exposed API in `UserEventTrackerService` is `saveLogs()`. This method sends logs to the server in chunks (which I'll explain in the next section) and triggers `resetLogs$` to clear previously stored logs. At first glance, `resetLogs$` might seem unnecessary since logs should be accumulate on the client until the app is closed. However, in our use case, we needed to also send logs when the user refreshed the application, clear previous ones, and start accumulating logs again. ```TS export class App { private userEventTrackerService = inject(UserEventTrackerService); @HostListener('window:beforeunload') onPageRefresh() { this.userEventTrackerService.saveLogs(); } } ``` ### Sending Logs To The Server At first, you might think sending data to the backend is as simple as injecting `HttpClient` and calling `post()`, which would create an [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest%5FAPI/Using%5FXMLHttpRequest?ref=angularspace.com) to save the data. However, the issue with XHR is that it gets canceled if the browser is closed and no analytics would be saved. There are two better alternatives. The first is the [sendBeacon() Navigator API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon?ref=angularspace.com), which allows sending a POST request even when the page is unloading (closing). The downside is that `sendBeacon()` doesn’t allow headers, authentication or cookies to be included in the request. The second and more flexible approach is using a standard `fetch()` request, where we can set up custom headers and enable the [keepalive property](https://developer.mozilla.org/en-US/docs/Web/API/Request/keepalive?ref=angularspace.com) by setting it to `true`. It's important to note that the request will be sent to the server when the application is closed/refreshed, but its success depends entirely on the server. This means that the frontend resets the accumulated logs once the request is issued, and if the server fails to complete the request, the logs will be lost. ```TS private sendToRemote(body: unknown): void { const xsrfToken = this.getCookie('XSRF-TOKEN'); fetch('api/logs', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(xsrfToken ? { 'X-XSRF-TOKEN': xsrfToken } : {}), }, credentials: 'include', keepalive: true, // keep the connection alive when app closes body: JSON.stringify(body), }); } ``` ## Component HTML Structure You can think of our application as an airline ticketing portal, a form based system spread across multiple routes, where users input details step by step, the final page is the checkout page, however the user can cancel his order on any step. ```HTML
Email Gender Man Woman Option 1 Option 2
``` The challenge is that even if you set up click or focus listeners on these HTML elements, you need to capture two key pieces of information: 1. The **value -** inside the HTML element (which is relatively easy to retrieve). 2. The **associated label** \- of the HTML element (which is much harder to obtain). Labels are tricky because, for elements like `input` or `mat-select`, the label is usually positioned next to the element. But what about radio buttons, checkboxes, or standard buttons? What counts as their label? Your first instinct might be to use the text inside the element. For example, a `button` labeled "Submit" would store `"Submit"` as its label. But this introduces a new challenge: localization. If the app supports multiple languages, debugging user events becomes more complex since the label text would be different depending on the language. What’s labeled `"Submit"` in English might be `"Enviar"` in Spanish or `"Soumettre"` in French, making it harder to track and analyze events consistently across different locales. ### Naming HTML Elements One solution that was proposed, though not ideal, was to apply [aria-label to name elements](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label?ref=angularspace.com). This approach required manually adding `aria-label` (or any kind of selector) to buttons, inputs, selects, and any other interactive elements we wanted to track. The `aria-label` enhances accessibility, however it can confuse screen readers if the naming is incorrect. Additionally, it should always be localized. I heard a saying which goes "a website without arial is still better than a website full of false ones". The goal is to have a selector that remains in English and is ignored by screen readers. Instead of using `aria-label` selector, we can introduce the [data-label attribute](https://developer.mozilla.org/en-US/docs/Learn%5Fweb%5Fdevelopment/Howto/Solve%5FHTML%5Fproblems/Use%5Fdata%5Fattributes?ref=angularspace.com). ```HTML
Email Gender Man Woman Option 1 Option 2
``` In my opinion, `data-label` would be the best approach to name elements, however if it is not feasible, in that case maybe keep using the nearest text to the component as its label and ignore the localization problem. I also want to highlight the position of `data-label` on different elements, such as `mat-select`. When opening mat-select, it creates an overlay where the options are displayed. However, this overlay prevents access to the `data-label` attribute if it is placed on the `mat-select` itself, making it difficult to retrieve. Therefore, applying it to the `mat-option` proved to be a more effective solution for reading the label of the select. ## Create Global Listeners The simplest approach to tracking HTML element interactions was to create a single service that listens to DOM events and logs interactions based on the element type. There were several iterations, but our first merged version has a similar structure to this: ```TS @Injectable({ providedIn: 'root', }) export class UserEventListenerService { private readonly userEventTrackerService = inject(UserEventTrackerService); private readonly document = inject(DOCUMENT); private readonly ngZone = inject(NgZone); private readonly router = inject(Router); private readonly dialog = inject(MatDialog); start() { afterNextRender(() => { merge( // open dialog log this.dialog.afterOpened.pipe(map((dialogRef) => ({ type: 'openDialog' }))), // close dialog log this.dialog.afterAllClosed.pipe(map(() => ({ type: 'closeDialog' }))), // router change log this.router.events.pipe( filter((e): e is NavigationEnd => e instanceof NavigationEnd), map((routerData) => ({ type: 'routerChange', text: routerData['url'] })), ), ).subscribe((res) => this.userEventTrackerService.createLog(res)); this.ngZone.runOutsideAngular(() => { // listen on click events this.document.addEventListener('click', (event) => { const target = event.target as HTMLElement; if (target.tagName === 'A') { this.userEventTrackerService.createLog({ type: 'clickElement', elementType: 'LINK', value: target.dataset['label'] ?? 'Unknown', }); } // ... other elements }, true); // listen on input change events this.document.addEventListener('change', (event) => { const target = event.target as HTMLElement; if (target.tagName === 'INPUT') { this.userEventTrackerService.createLog({ type: 'inputChange', elementType: 'INPUT', elementLabel: target.dataset['label'] ?? 'Unknown', value: (target as HTMLInputElement).value, }); } // ... other elements }, true); }); }) } } ``` The real service is obviously larger and there are multiple conditions to distinguish what type of (`tagName`) HTML element was interacted with. This is a reduced version for better overview. A few key points to highlights: - **Merging Observables:** The `merge()` operator to combine logs for dialog open/close events, as well as navigation changes to capture them. - **Event Listeners for Click & Value Changes:** The `addEventListener` listens for both clicks and value changes, and conditionally log the HTML element interactions based on the `tagName`. Additionally, the [capture phase is set to true](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener?ref=angularspace.com) so the event is caught as it bubbles up. Without this, some events, like radio button changes, weren't caught. - **Running Outside Angular's Change Detection:** The [runOutsideAngular](https://angular.dev/api/core/NgZone?ref=angularspace.com) executes tasks outside Angular’s change detection cycle for performance reasons. It’s generally a good practice to prevent unnecessary re-renders. - **Prevent SSR Failure**: The [afterNextRender](https://angular.dev/api/core/afterNextRender?ref=angularspace.com) is triggered only on the client side, once by Angular finishes rendering. It's a recommended way to register a callback that manipulates with DOM elements. The major benefit of this approach is that we have one globally registered service that will handle the whole application logging as the user interacts with DOM element. ```TS bootstrapApplication(App, { providers: [ // ... other things ... provideAppInitializer(() => { inject(UserEventListenerService).start(); }), ], }); ``` ## Using Directives for More Specific Use Cases The slight problem, at least I couldn’t solve, was that not everything is possible to handle globally. Example is form submission. While you can still setup global listener on the `submit` event, and you can get even the validity, I couldn’t get the form structure, the form control names and the values inside the form. ```TS this.document.addEventListener('submit', (event) => { const formElement = event.target as HTMLFormElement; const isValid = formElement.checkValidity(); const formStructure = "Dunno"; // please help }, true); ``` The problem is that the `event.target` is a type of `HTMLFormElement`, and ideally we want the `FormGroup` type to then get the form’s `value`. To handle a use case like this directives are the best answerers. ```TS @Directive({ selector: 'form[formGroup]', standalone: true, host: { '(ngSubmit)': 'onSubmit()', }, }) export class FormSubmitDirective { private formGroupDirective = inject(FormGroupDirective); private userEventTrackerService = inject(UserEventTrackerService); onSubmit() { const form = this.formGroupDirective.form; const isValid = form.valid; const values = form.getRawValue(); if (isValid) { this.userEventTrackerService.createLog({ type: 'formSubmitValid', values, }); } else { this.userEventTrackerService.createLog({ type: 'formSubmitInvalid', values, fieldValidity: getFormValidationState(form), }); } } } ``` To use the `FormSubmitDirective` directive, you have to import it into each standalone component that displays the form. The `ngSubmit` can be triggered as long as the `
` tag is properly formed and contains some interactive element, via keyboard or clicking on the submit button ``, otherwise the directive will not catch the form submission. The `getFormValidationState()` is a custom util that goes thought the whole form structure, keep the form keys, but replace the values with a `VALID / INVALID` string whether the field is filled properly. ```TS type ValidationState = | 'VALID' | 'INVALID' | { [key: string]: ValidationState } | ValidationState[]; const getFormValidationState = (form: AbstractControl): ValidationState => { if (form instanceof FormControl) { return form.valid ? 'VALID' : 'INVALID'; } if (form instanceof FormGroup) { return Object.keys(form.controls).reduce( (acc, key) => ({ ...acc, [key]: getFormValidationState(form.controls[key]), }), {}, ); } if (form instanceof FormArray) { return form.controls.map((control) => getFormValidationState(control)); } // default use case, shouldn't happen return 'INVALID'; }; ``` When submitting an invalid form, where required fields are left empty (or they have any validation error), the form’s invalid state will generate a log like the following: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/02/article-form-validity-util-result.png) Form Validity Result ### Directives For Input Changes You may be wondering why didn't we initially go with directives for HTML element changes, something like this ```TS @Directive({ selector: 'input', standalone: true, host: { '(change)': 'onChange($event)', }, }) export class EventInputsDirective { private userEventTrackerService = inject(UserEventTrackerService); onChange(event: FocusEvent) { const inputTarget = event.target as HTMLInputElement; const labelName = inputTarget.dataset['label'] ?? 'Unknown'; this.userEventTrackerService.createLog({ type: 'inputChange', elementType: inputTarget.tagName, elementLabel: labelName, value: inputTarget.value, }); } } ``` This would indeed work. The only drawback we saw was that you would have to import this (and other) directives into each standalone component that you want to track compared to the service that handles input changes globally. ## Catch Network By Interceptors You're likely already familiar with interceptors, but for the sake of completeness, let’s quickly demonstrate how to capture incoming and outgoing requests. The log will capture the full URL, status codes, and the time taken, allowing you to analyze the time between when the request is sent and when the response is received. ```TS export const userEventLoggingInterceptor = ( req: HttpRequest, next: HttpHandlerFn, ): Observable> => { const service = inject(UserEventTrackerService); return next(req).pipe(tap((event) => { // send http event if (event.type === HttpEventType.Sent) { service.createLog({ type: 'apiCall', url: req.urlWithParams }); } // receive http event else if (event.type === HttpEventType.Response) { trackingService.createLog({ type: 'apiResponse', url: req.urlWithParams, status: event.status, }); } }), ); }; ``` ```TS bootstrapApplication(App, { providers: [ provideHttpClient(withInterceptors([userEventLoggingInterceptor])), // ... other ... ] }) ``` ## Summary In this article I tried to go though a real life web application problem and propose a suitable solution to it. The steps cover how to create a general service that listens on DOM element interactions, identify HTML elements with `data-label`, create an interceptor to catch network calls, use directives for elements which can not be covered globally, or just manually next a subject to create a custom log. The last thing I want to briefly touch on is that collecting user data may violate GDPR, so you should obtain user consent before proceeding with data collection. I hope you liked this article. The whole event tracker is [available on Github](https://github.com/krivanek06/stackblitz-angular-user-event-logging?ref=angularspace.com), you are free to use it in your own projects. Feel free to share your thoughts, read more from me on [dev.to](https://dev.to/krivanek06?ref=angularspace.com) or connect with me on [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/02/Screenshot-2024-11-13-at-15.52.00--1---3---1-.jpg) ### Learning Angular 2x Paperback Giveaway URL: https://www.angularspace.com/learning-angular-2x-paperback-giveaway/ Last updated: 2025-02-14T13:37:15.000Z I have received Learning Angular Fifth Edition Paperback by [Aristeidis Bampakos](https://www.linkedin.com/feed/?ref=angularspace.com) from [Packt](https://www.linkedin.com/feed/?ref=angularspace.com) & I have 2x more to giveaway!!!! Learning Angular is now full released and out of early access!! _This post is for subscribers only._ ### Loading Angular Resources On-Demand: A Progressive Guide to Dynamic Data Fetching URL: https://www.angularspace.com/loading-angular-resources-on-demand-a-progressive-guide-to-dynamic-data-fetching/ Last updated: 2025-02-06T13:30:17.000Z ## Part 1: Setting the Stage - Basic Resource Fetching Angular's `resource()` method provides a straightforward way to handle asynchronous data fetching. We'll start with the most basic scenario: fetching weather data using the `resource` method. This will set the foundation for more advanced techniques. Let's start with the basic component. We'll define a resource that fetches data, and have a button that refetches the data when clicked. We'll also display the loading state and weather information once the data is loaded. Here’s the initial component code: ```typescript import { Component, resource } from '@angular/core'; interface WeatherData { temperature: number; condition: string; icon: string; } @Component({ selector: 'app-weather-info', template: `
@if (weatherResource.isLoading()) { } @else if (weatherResource.value()) { weather icon

Temperature: {{ weatherResource.value()?.temperature }}

Condition: {{ weatherResource.value()?.condition }}

}
`, }) export class WeatherInfoComponent { weatherResource = resource({ loader: async ({ abortSignal }) => { const response = await new Promise((resolve) => { setTimeout(() => { fetch('assets/weather.json', { signal: abortSignal }).then((r) => resolve(r) ); }, 1500); }); if (!response.ok) { throw new Error('Could not fetch data'); } const data = await response.json(); return data as WeatherData; }, }); } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/simple-one.gif) **Explanation:** 1. **Imports:** We import `Component` and `resource` from `@angular/core`. 2. **Types:** The `WeatherData` interface is defined to match the structure of the data we expect from the JSON file. 3. **Component Properties:** - `weatherResource`: This is a `Resource` that manages the asynchronous data fetching process. The result of the `resource()` method is assigned to it. 4. **`weatherResource` Initialization:** - The `resource()` method is called to initialize `weatherResource` when the component is instantiated. This also sends our first HTTP call via the loader function. - The `loader` option is defined as an asynchronous function. This function simulates an API call by fetching data from `assets/weather.json` with a 1.5-second delay using `setTimeout`, and includes an `abortSignal` to handle cancellations. - If the network response isn't ok, it will throw an error, that the `resource()` method will handle. - If everything goes well, the data is returned. 5. **Template:** - A button is present, that calls the `weatherResource.reload()` method. This will re-trigger the loader function, fetching the data again. - The template uses `@if` blocks, and the `weatherResource` methods `isLoading()`, `value()` to show a loading state or the data. **Key Takeaways:** - The `resource()` method simplifies asynchronous data fetching by managing the loading state, handling errors, and providing the data once it has been loaded. - The template is reactive, updating automatically when the resource's state changes. - We are using `weatherResource.reload()` method to re-trigger the data fetch. ## Part 2: Loading Resources On-Demand In the previous example, the `resource` was initialized immediately when the component was created, triggering a data fetch. What if you want to defer this fetch until a specific action? You can use the `request` attribute to control *when* the resource's `loader` is executed, allowing you to load resources on-demand. Let's modify our previous example to include a `weatherRequestState` signal. This signal will determine if the resource's loader should be triggered. This way, we can load our resource on-demand based on user interactions. Here's the updated code: ```typescript import { Component, resource, signal } from '@angular/core'; interface WeatherData { temperature: number; condition: string; icon: string; } type WeatherRequestState = 'idle' | 'ready'; @Component({ selector: 'app-weather-info', template: `
@if (weatherResource.isLoading()) { } @else if (weatherResource.value()) { weather icon

Temperature: {{ weatherResource.value()?.temperature }}

Condition: {{ weatherResource.value()?.condition }}

}
`, }) export class WeatherInfoComponent { weatherRequestState = signal('idle'); weatherResource = resource({ request: () => { if (this.weatherRequestState() === 'idle') { return undefined; } return this.weatherRequestState(); }, loader: async ({ abortSignal }) => { const response = await new Promise((resolve) => { setTimeout(() => { fetch('assets/weather.json', { signal: abortSignal }).then((r) => resolve(r) ); }, 1500); }); if (!response.ok) { throw new Error('Could not fetch data'); } const data = await response.json(); return data as WeatherData; }, }); getWeatherInfo() { if (this.weatherRequestState() !== 'ready') { this.weatherRequestState.set('ready'); } else { this.weatherResource.reload(); } } } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/lazily-loaded.gif) **Changes:** - We've introduced a `weatherRequestState` signal to control when the resource should load. The state can be `'idle'` or `'ready'`. - The `request` attribute of the resource is now defined as a function that returns the `weatherRequestState` value, or `undefined` if the state is `'idle'`. - The `loader` function will only be called when the `request` function returns `'ready'`. - The `getWeatherInfo()` method now checks if the current state is `'idle'`, in that case it sets the `weatherRequestState` to `'ready'`, otherwise it calls `weatherResource.reload()` to refetch data. **Key Takeaways:** - We can use the `request` attribute to load the resource on-demand, only when needed. - The `loader` function will only run when the `request` function returns a value different than `undefined`. ## Part 3: Handling Errors Data fetching isn't always smooth, so it's important to handle potential errors gracefully. Let's extend the application to include error handling, and to simulate an error when fetching the data. We'll add a new button that attempts to fetch data with an error. This will demonstrate how to display error messages. Here's the modified code: ```typescript import { Component, resource, signal } from '@angular/core'; interface WeatherData { temperature: number; condition: string; icon: string; } type WeatherRequestState = 'idle' | 'ready' | 'simulateError'; @Component({ selector: 'app-weather-info', template: `
@if (weatherResource.isLoading()) { } @else if (weatherResource.error()) { } @else if (weatherResource.value()) { weather icon

Temperature: {{ weatherResource.value()?.temperature }}

Condition: {{ weatherResource.value()?.condition }}

}
`, }) export class WeatherInfoComponent { weatherRequestState = signal('idle'); weatherResource = resource({ request: () => { if (this.weatherRequestState() === 'idle') { return undefined; } return this.weatherRequestState(); }, loader: async ({ abortSignal, request: requestState }) => { const response = await new Promise((resolve) => { setTimeout(() => { fetch('assets/weather.json', { signal: abortSignal }).then((r) => resolve(r) ); }, 1500); }); if (!response.ok) { throw new Error('Could not fetch data'); } if (requestState === 'simulateError') { throw new Error('Something went wrong'); } const data = await response.json(); return data as WeatherData; }, }); getWeatherInfo() { if (this.weatherRequestState() !== 'ready') { this.weatherRequestState.set('ready'); } else { this.weatherResource.reload(); } } getWeatherInfoWithError() { this.weatherRequestState.set('simulateError'); } } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/with-error.gif) **Changes:** - We've introduced a `simulateError` state to the `WeatherRequestState` type. - The `weatherRequestState` signal is now used to control if the loader should simulate an error. - The `loader` function now checks if the request state is `'simulateError'`, and if it is, it throws a new error. - The `getWeatherInfoWithError` method is introduced to set the `weatherRequestState` to `simulateError`, triggering a data fetch with a simulated error. - The template is updated to display the error using `weatherResource.error()`. **Key Takeaways:** - We're using the `weatherRequestState` signal to control the initial loading of the data, and to simulate errors on the loader. - The `resource()` method will catch errors thrown in the `loader` function, and make them available through the `error()` signal. - The template uses `weatherResource.error()` to display the error message. ## Part 4: Dynamic Resource Switching (Multi-City Support) Let's now introduce more dynamic behavior into our component by enabling the user to switch between **single** and **multi-city** modes. When switching modes, we'll modify how the resource fetches the data, using the `request` option to control the fetching process. We'll add a toggle switch and a dropdown to allow the user to select their desired city when in multi-city mode. Here’s the complete component code: ```typescript import { Component, resource, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; type City = 'Stockholm' | 'Milan'; interface WeatherData { temperature: number; condition: string; icon: string; city?: City; } type WeatherRequestState = 'idle' | 'ready' | 'simulateError'; type WeatherResourceConfig = { requestState: WeatherRequestState; isMultiCityMode: boolean; selectedCity: City; }; @Component({ selector: 'app-weather-info', imports: [FormsModule], template: `
@if (isMultiCityMode()) { } @else { } @if (weatherResource.isLoading()) { } @else if (weatherResource.error()) { } @else if (weatherResource.value()) { weather icon

Temperature: {{ weatherResource.value()?.temperature }}

Condition: {{ weatherResource.value()?.condition }}

}
`, }) export class WeatherInfoComponent { weatherRequestState = signal('idle'); isMultiCityMode = signal(false); cities: City[] = ['Stockholm', 'Milan']; selectedCity = signal(this.cities[0]); weatherResource = resource< WeatherData | undefined, WeatherResourceConfig | undefined >({ request: () => { if (this.weatherRequestState() === 'idle') { return undefined; } return { requestState: this.weatherRequestState(), isMultiCityMode: this.isMultiCityMode(), selectedCity: this.selectedCity(), }; }, loader: async ({ abortSignal, request }) => { if (!request) { return undefined; } const { requestState, isMultiCityMode, selectedCity } = request; const response = await new Promise((resolve) => { setTimeout(() => { const url = isMultiCityMode ? 'assets/weather-multi.json' : 'assets/weather.json'; fetch(url, { signal: abortSignal }).then((r) => resolve(r)); }, 1500); }); if (!response.ok) { throw new Error('Could not fetch data'); } if (requestState === 'simulateError') { throw new Error('Something went wrong'); } const data = await response.json(); if (isMultiCityMode) { const weatherInfo = (data as WeatherData[]).find( (info) => info.city === selectedCity ); if (!weatherInfo) { throw new Error('Weather info not found'); } return weatherInfo; } return data as WeatherData; }, }); getWeatherInfo() { if (this.weatherRequestState() !== 'ready') { this.weatherRequestState.set('ready'); } else { this.weatherResource.reload(); } } getWeatherInfoWithError() { this.weatherRequestState.set('simulateError'); } } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/multi-city.gif) **Changes:** - **`FormsModule`**: It is imported to use the `ngModel` with the toggle, and the select element. - **`cities` and `selectedCity`:** The `cities` array is created, as well as the `selectedCity` signal to store the selected city. - **`isMultiCityMode`:** A signal to store whether the user wants to use multi-city mode or not. - The `request` attribute now sends the `weatherRequestState`, `isMultiCityMode`, and `selectedCity` to the loader. - **Template:** - A toggle is added to switch between the single and multi city modes. - A select element is displayed when the multi city mode is enabled to select the desired city. - **`getWeatherInfo` Method:** This method checks if the multi mode is enabled or disabled, and sets the `weatherRequestState` signal to `ready`, or reloads the data. - **`getWeatherInfoWithError()` Method:** Sets the `weatherRequestState` to `simulateError`, triggering a fetch with error in single mode. - The `loader` now fetches different data based on the `isMultiCityMode`. If it's true, it will fetch data from a `weather-multi.json`, and filter the data by the selected city. **Key Takeaways:** - We're now controlling the loading of the data through the `weatherRequestState` signal, initializing the resource on-demand. - The `request` attribute can be used to send more data to the loader. - The `loader` now handles different scenarios: loading, errors, single city, and multi city, using the value sent by the `request` attribute. ## Conclusion This progressive guide has showcased how to utilize Angular's `resource()` method for fetching data, handling errors, and dynamically switching between different fetching behaviors. By gradually building upon basic examples, we've explored how to manage diverse scenarios and how to control the loading of resources on-demand. We've also seen how to use the `request` attribute to send more information to the `loader`, adding flexibility to our data fetching process. > **Important Note:** The examples in this article are for illustrative purposes and to demonstrate the capabilities of the `resource` API. In a production application, it's recommended to abstract HTTP calls into dedicated Angular services and handle error scenarios in a more robust and centralized manner. Additionally, avoid simulating errors directly within component logic. ## Btw > Note: Shameless plugs below... - If you're new to Angular, check out my [FREE 90 minutes Angular Crash Course](https://youtu.be/oUmVFHlwZsI?ref=angularspace.com) (people love it ❤️) - If you want to dive deep into Angular with over 90 projects, check out the [Angular Cookbook](https://ng-cookbook.com/?ref=angularspace.com) - And if you want to dive deep into Angular, check out my book [(Modern Angular: Mastering Signals)](https://codewithahsan.dev/books?ref=angularspace.com). I'm working on it at the time of writing this article. Hope this article helps you with a leap forward in your Angular journey. As always, happy coding! --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/02/Screenshot-2024-11-13-at-15--1-.jpg) ### Migrating a Large Angular Application to Standalone URL: https://www.angularspace.com/migrating-a-large-angular-application-to-standalone/ Last updated: 2025-01-29T14:07:49.000Z ## Introduction Recently, I shared my experience migrating a large Angular application to standalone components in [a Twitter thread](https://x.com/Armandotrue/status/1868955605840674955?ref=angularspace.com). This sparked a lot of interest, so I expanded it into a more detailed guide. Migrating a complex Angular application to use standalone building blocks is a significant endeavor, and, in my experience, is not to be approached lightly. However, on the other hand, it is also beneficial to the project's code quality and the mental health of the developers involved (this is only half a joke). In this article, I’ll provide a deeper dive into the process, including the steps I undertook, the challenges I encountered, and the solutions I implemented. ## Application Overview Let's start by first examining the application that I migrated: - **Angular Version:** 17 (must be Angular 15.2.0 or later). - **Application domain:** HR management system which incorporates lots of features like attendance tracking, leave management, hiring, integrations with external services like Microsoft Teams and Calendar and so on. - **Structure:** Over 1,000 interconnected components, directives, and pipes, 500+ `NgModule`\-s. - **Dependencies:** Numerous external libraries and packages I could talk a lot about the different complexities of this application. however, I think it will be easier to just show an excerpt from the `package.json` file: ```json "dependencies": { "@angular/animations": "^17.3.11", "@angular/cdk": "^16.2.14", "@angular/common": "^17.3.11", "@angular/compiler": "^17.3.11", "@angular/core": "^17.3.11", "@angular/forms": "^17.3.11", "@angular/localize": "^17.3.11", "@angular/material": "^16.2.0", "@angular/platform-browser": "^17.3.11", "@angular/platform-browser-dynamic": "^17.3.11", "@angular/platform-server": "^17.3.11", "@angular/router": "^17.3.11", "@angular/service-worker": "^17.3.11", "@auth0/angular-jwt": "^5.0.2", "@azure/msal-angular": "^3.0.8", "@azure/msal-browser": "^3.5.0", "@babel/polyfill": "^7.12.1", "@ckeditor/ckeditor5-angular": "^8.0.0", "@kolkov/angular-editor": "^2.0.0", "@microsoft/applicationinsights-web": "^2.6.5", "@microsoft/signalr": "^8.0.0", "@ng-select/ng-select": "^12.0.7", "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", "@raiser/raiser-integration": "17.0.0-rc.1", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-datatable": "^20.1.0", "ajv": "^8.12.0", "angular-cropperjs": "^14.0.1", "bootstrap": "^4.0.0", "chart.js": "^4.4.0", "chartjs-plugin-stacked100": "^1.5.3", "chartjs-plugin-zoom": "^2.0.1", "ckeditor5": "^42.0.2", "core-js": "^3.16.2", "cropperjs": "^1.6.1", "d3": "^7.0.0", "dayjs": "^1.11.11", "file-saver": "2.0.5", "font-awesome-scss": "^1.0.0", "hammerjs": "^2.0.8", "jsplumb": "^2.15.6", "ng-click-outside2": "^15.0.1", "ng2-file-upload": "^5.0.0", "ngx-bar-rating": "^7.0.1", "ngx-clipboard": "^16.0.0", "ngx-color-picker": "^14.0.0", "ngx-drag-scroll": "^17.0.1", "ngx-ellipsis": "^4.1.3", "ngx-image-cropper": "^1.3.8", "ngx-infinite-scroll": "^17.0.1", "ngx-mask": "^15.2.1", "ngx-scrollbar": "^13.0.3", "ngx-ui-switch": "^14.1.0", "powerbi-client-angular": "^3.0.5", "primeicons": "^7.0.0", "primeng": "^17.18.0", "rxjs": "^7.4.0", "tslib": "^2.3.1", "zone.js": "^0.14.7" }, ``` Yes, you are seeing it right: this project has 59 (!) dependencies. And all of them are used in lots of the components of the app. So, naturally, I was scared to start the migration process. But, anyway, we did it, although, with some initial preparations. ## Before Migrating First, before we started, we performed three crucial steps: 1. Run an initial test migration to see if it actually works. We rolled it back next and realized this is achievable. 2. Inform the team that such a migration is being done and it might take up to several days. Instruct other devs to use standalone components when they author new ones during the development of their tasks. 3. Update the branch to the latest version and search for all the `NgModule` instances in the project. This is not a very important step, however, it gave us idea of how much the schematic helped subsequently. After doing this, we began the migration. ## Initial Migration Steps The migration began with the execution of Angular's official schematic, as detailed in the [Angular documentation](https://angular.dev/reference/migrations?ref=angularspace.com#standalone-migration). As instructed, we ran the following commands: ```bash ng g @angular/core:standalone # and select "Convert all components, directives and pipes to standalone" ng g @angular/core:standalone # and select "Remove unnecessary NgModule classes" ng g @angular/core:standalone # and select "Bootstrap the project using standalone APIs" ``` This automated process removed approximately 400 `NgModule` instances, marked components as standalone, and updated the bootstrap configuration in `main.ts`. Here it is important to note that you have to run the schematic 3 times: 1. First to mark everything as standalone and move those components/pipes/directives to the `imports` array of their respective modules instead of the `declarations` array. 2. Next to remove the NgModules and move the now standalone components to direct imports of other standalone components where they are used. 3. Finally, to remove the `AppModule` and use the standalone providers like `provideRouter`, `ProvideHttpClient`, etc. After each run, we did a prod build locally to ensure nothing seriously broke (it did). After fixing the issues, we ran the schematic again. Finally, after going through these steps, we were left with an application that... kinda worked. Not really, because afterward, the real challenges began. Let's explore those now. ## Challenges and Solutions ### Circular Dependencies Post-migration, circular dependencies became a small issue. Thankfully, there is an easy fix. Leveraging Angular's `forwardRef` function. For an in-depth explanation, refer to [this resource](https://timdeschryver.dev/blog/fixing-angular-circular-dependencies?ref=angularspace.com). ### Service Providers Services previously provided within specific modules needed adjustments. We used a global search-and-replace operation updated the `@Injectable` decorator to `{providedIn: 'root'}`, ensuring consistent service availability across the application. Now, let us talk a bit about this, because it might cause some doubts among readers. Yes, it is, in fact, perfectly okay to mark all of your services as `providedIn: 'root'`. One might wonder if this won't make the services initialize sooner than intended (for instance, a service might have been a part of a lazily loaded module and now became provided globally). However, the Angular DI system is smart enough to create service instances when they are requested, so this must not be an issue. If we think about it, if `SomeService` is provided in `SomeModule` and injected for the first time in `SomeComponent` (which has to be a part of `SomeModule` by either being declared on it or exported to it to be able to inject `SomeService`), then removing `SomeModule` won't change anything, since we can easily refactor `SomeComponent` to itself become lazily-loaded and still be the first to inject `SomeService`. ### Residual `NgModule` Instances Certain modules, particularly the dreaded `SharedModule`\-s, persisted after the schematic execution. Yes, the schematic does not remove **all** the `NgModule`\-s. Usually, it does not remove modules that are imported by other modules repeatedly, because, as far as I understand, it cannot tell if it is safe to remove it (Component A in Module B might import Component C from Module D via a Shared Module which imports Module D and is then imported into Module B). So, what can be done about it? Well, it turns out we only have two options: either to extensively investigate the application to remove all intermediary modules on-by-one, or just nuke all the remaining modules and deal with the fallout. We took the second approach which, in retrospect, turned out to be correct. Yes, after removing so many `NgModule`\-s in one go, we initially faced hundreds of errors in the build result. However, most of them could be fixed automatically. Some would be fixed manually, but still very quickly, because the build output would point us to the exact location of a missing import. So, I advise doing just that - this "manual" labor will take way shorter than you might imagine - took just 20 minutes for us. shorter than you might imagine - took just 20 minutes for us. ### Routing Configuration The schematic did not automatically convert routing modules used for lazy loading. I manually: 1. Renamed `.routing.module.ts` files to `.routes.ts`. 2. Removed the `NgModule` decorator. 3. Exported the routes directly. 4. Updated lazy-loaded imports to match the new configuration. This ensured that the routing system aligned with Angular’s standalone paradigm. Also, you can consider using AI to do this - the required steps are very clearly cut, and it is fairly easy to explain to a machine what to do. I would welcome the readers of this article to copy-paste the steps above into Copilot Editor Chat (or any other AI software you use for development) and share the results in the comments! ## Runtime Issues and Resolutions ### Directive Binding Syntax A significant challenge involved directives bound without bracket syntax (`[]`). Let me show a small example of what I mean: ```html
``` Angular failed to recognize such "bracketless" directives unless explicitly imported into the component's template, leading to runtime errors. This is quite a tricky problem, as you will not get build-time errors and need to navigate to the exact components where this "bad" directive binding is used (for further details on this issue, please consult [this discussion](https://github.com/angular/angular/issues/12345?ref=angularspace.com)). So, how can it be fixed? Well, if you have extensive unit tests, you can run them and fix issues that way. However, this won't cover you 100%, and, in addition, migrating to standalone might cause issues with unit tests imports, so you might first need to fix them before you can take this approach. Because this project did not have any unit tests (not my fault!), I took another approach and spent 10-15 minutes utilizing the help of some AI to conjure up a script that would: 1. Accept a directive/component selector 2. Search for all the instances of this selector in the project's `.html` files 3. Filter out the corresponding `.component.ts` files that *do not* have this directive/component imported 4. Report file paths to those components This resulted in a very quick fix of all missing imports > To clarify, Angular's schematic did import necessary components, but only in 90-95% of cases. I am unsure why it missed some instances, however, you can use this script to easily fix the rest. Here is the script: ```javascript const fs = require('fs'); const path = require('path'); async function findMissingImports(projectRoot, directiveName, directiveClass) { const missingImportFiles = []; const searchDirectory = async (dir) => { const files = fs.readdirSync(dir); for (const file of files) { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { await searchDirectory(filePath); // Recursive call for subdirectories } else if (file.endsWith('.component.html')) { const tsFilePath = filePath.replace('.component.html', '.component.ts'); // Check if the corresponding .ts file exists if (!fs.existsSync(tsFilePath)) continue; // 1. Check if the directive is used in the HTML file const htmlContent = fs.readFileSync(filePath, 'utf-8'); const directiveUsed = htmlContent.includes(directiveName); if (!directiveUsed) continue; // 2. Check if the directive class is imported in the .ts file const tsContent = fs.readFileSync(tsFilePath, 'utf-8'); const importRegex = new RegExp(`import\\s+\\{.*\\b${directiveClass}\\b.*\\}\\s+from\\s+.*`); const importPresent = importRegex.test(tsContent); // 3. If the directive is used but the import is missing, add to the list if (directiveUsed && !importPresent) { missingImportFiles.push(tsFilePath); } } } }; await searchDirectory(projectRoot); return missingImportFiles; } // --- Configuration --- const PROJECT_ROOT = './'; // Replace with your project path const DIRECTIVE_NAME = "someDirective"; const DIRECTIVE_CLASS = "SomeDirective"; // --- Run the script --- findMissingImports(PROJECT_ROOT, DIRECTIVE_NAME, DIRECTIVE_CLASS) .then((missingFiles) => { if (missingFiles.length > 0) { console.log('Component files missing the import:'); missingFiles.forEach((file) => console.log(file)); } else { console.log('No component files found missing the import.'); } }) .catch((err) => { console.error('An error occurred:', err); }); ``` You can also find the script in a GitHub Gist [here](https://gist.github.com/Armenvardanyan95/93e5e3ce8d721ded9b294bbedc429296?ref=angularspace.com). If this article helped you decide to start migrating your Angular app to standalone, I encourage you to copy this script and utilize it for a smoother transition. ## Results Now, I just want to sum up the results of the migration: - **Time Spent:** - *On Migration itself:* 2 not full day, around 10 hours total - *On bug fixing after the migration:* around 2 weeks, however, it is important to note that only around 10% of the bugs were actually related to the migration itself, and others were just other random issues. Most of the migration-related bugs were just more missing imports, super easy to fix - **Performance gains:** Migrating to standalone helped us improve the build time by switching to ESBuild, which was previously very difficult because of some issues several of our project's modules had. We couldn't figure those issues out, but with `NgModule`\-s gone, we switched to ESBuild in literally 10 minutes and hugely reduced the time our pipelines ran. - **Improved code architecture:** Obviously the biggest gain, the project suffered from many messy interconnected bits which got simplified with standalone adoption. > Note: also consider adding the [strictStandalone](https://angular.dev/reference/configs/angular-compiler-options?ref=angularspace.com#strictstandalone) option in the `tsconfig.json` file to enforce authoring only standalone components in the future. ## Conclusion Migrating a large-scale Angular application to standalone components is a complex but very rewarding process. By anticipating potential challenges and applying targeted solutions, as outlined above, Angular developers can achieve a simpler and more maintainable codebase. If you want to see the original discussion that inspired this article, check out [my Twitter thread](https://x.com/Armandotrue/status/1868955605840674955?ref=angularspace.com). ## Small Promotion ![Gg2RPJKWwAAHSId.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Gg2RPJKWwAAHSId.png) My book, Modern Angular, is now in print! I spent a lot of time writing about every single new Angular feature from v12-v18, including enhanced dependency injection, RxJS interop, Signals, SSR, Zoneless, and way more. If you work with a legacy project, I believe my book will be useful to you in catching up with everything new and exciting that our favorite framework has to offer. Check it out here: [https://www.manning.com/books/modern-angular](https://www.manning.com/books/modern-angular?ref=angularspace.com) P.S: Check out Chapter 2 of my book, "A Standalone Future", to learn about standalone architecture, APIs, and more details on the migration process ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2024-11-13-at-15.52.00--1---6-.jpg) ### Create Custom RxJs Operators URL: https://www.angularspace.com/create-custom-rxjs-operators/ Last updated: 2025-01-27T14:58:58.000Z On the recent Angular live interview in January 2025 a question was asked: "[Are we ditching Observables and RxJs?](https://www.youtube.com/live/apyVs0D6rf0?t=3228s&ref=angularspace.com)". A simple answer is that RxJs had became optional, but it is far from obsolete. The introduction of signals has added a new tool to Angular's reactive programming toolbox, but RxJs remains valuable for scenarios involving complex data streams, event handling, and real-time updates. I've already written two articles in 2024 on Advanced RxJs Operators You Know But Not Well Enough - [part 1](https://dev.to/krivanek06/advanced-rxjs-operators-you-know-but-not-well-enough-1ela?ref=angularspace.com) and [part 2](https://dev.to/this-is-angular/advanced-rxjs-operators-you-know-but-not-well-enough-pt-2-3df5?ref=angularspace.com), which received positive feedback. Even though there are plenty of operators to choose from the official RxJs docs, there are times when you wish for a particular operator to solve a unique use case. In this article, I want to explore how you can create custom RxJs operators and then give you some operators (not just simple loggers) that may be useful to your application. Every operator that will be mentioned in the article can be found in the [Github Repository](https://github.com/krivanek06/stackblitz-rxjs-playground/tree/main/src/custom-rxjs-operators?ref=angularspace.com). Those customs operators include: - `filterNil()` \- filters out `null` and `undefined` values from the pipe chain - `dataPolling()` \- poll data from an API in some time interval - `handleError()` \- complex error handling by sending data to Sentry and notifying the user - `rememberHistory()` \- caching last N values and accessing previous state of the received data - `objectValueChanged()` \- deep object comparison, emitting only changed states compared to previous - `objectChangedFields()` \- listing out an object’s (form) changed fields - `loadingStatus()` \- similar to the `rxResource()` API, that works with Observables ## What Is An Operator? To answer this question, it's best to start by looking at the [Github RxJs implementations](https://github.com/ReactiveX/rxjs/tree/master/packages/rxjs/src/internal/operators?ref=angularspace.com) for all available operators. Let’s choose the most common `map()` operator and see how it is being implemented. At the moment of writing this article, in RxJs version 7.8.1, the `map()` operator implementation looks as follows: ```TS export function operate( { destination, ...subscriberOverrides }: OperateConfig ) { return new Subscriber(destination, subscriberOverrides); } export function map( project: (value: T, index: number) => R ): OperatorFunction { return (source) => new Observable((destination) => { // The index of the value from the source. let index = 0; // Subscribe to the source source.subscribe( operate({ destination, next: (value: T) => { // Call the projection function with the context, // and send the resulting value to the consumer. destination.next(project(value, index++)); }, }) ); }); } ``` This is nice and all, but what does this piece of code tell us? My main takeaway is that, the `map()` operator is simple a JavaScript function that returns a specific `OperatorFunction` type. The logic what exactly happens inside the function is now irrelevant. What I want to focus on is the returning type. Reading the [return type explanation on Github](https://github.com/ReactiveX/rxjs/blob/master/packages/observable/src/types.ts?ref=angularspace.com#L29), the authors explain that there are actually two operator types: `OperatorFunction` and `MonoTypeOperatorFunction`. The `OperatorFunction` type "*always takes a single parameter (the source Observable) and returns another Observable.*”. The reason why this type is used with the `map()` operator is that it takes some sort of generic value `T` as an input and returns a different output `R` value. This actually makes sense as we can write a predicate that transforms the entire shape of the input object `T -> R`. When the output type is the same type as the input type, we can use the `MonoTypeOperatorFunction`. It’s description says “*A function type interface that describes a function that accepts and returns a parameter of the same type.*”. Also worth pointing out that the `source` variable represents the Observable passed into the operator function when it is invoked within a pipe chain. Meaning when you use the `source.subscribe(value => ... )` then `value` represents the data flowing through the pipe chain into this function. ## Start With Basics End of the theory, let's dive into something practical to understand the basics. To create a simple custom operator, let’s say that we have a stream of numbers and want to multiply them by a specific value. Here's the result we aim to achieve: ```TS const source$ = of(1, 2, 3); source$.pipe(multiplyBy(6)).subscribe(console.log); // 6, 12, 18 ``` From what we know so far, we can create something like this: ```TS export function multiplyBy(val = 2): MonoTypeOperatorFunction { return (source: Observable): Observable => new Observable((subscriber) => { return source.pipe(map((d) => d * val)).subscribe({ next(value) { subscriber.next(value); }, error(err) { subscriber.error(err); }, complete() { subscriber.complete(); }, }); }); } ``` The function name is `multiplyBy()` which is used inside a pipe chain. It returns a `MonoTypeOperatorFunction` type because, although we modify the data, the type remains the same. Inside the function, we subscribe to the current Observable to access the values flowing through the pipe chain and return a new Observable. This implementation works fine, however, I didn’t like this extra layer of abstraction by creating an additional Observable wrapping around the `source` Observable, and also explicitly subscribing to the `source`. This approach is useful if we need full control over subscription management or have complex custom logic. Since none of these apply to our code, we can simplify our custom operator as follows: ```TS export function multiplyBy(val = 2): MonoTypeOperatorFunction { return (source) => source.pipe(map((d) => d * val)); } ``` The shorter syntax is more readable, so I will continue with it to list out some operators that may be useful in your projects, starting from simple ones up to more complicated ones. ## Custom RxJs Operators ### 1.) Filter Nil The inspiration for this operator came from the [ngxtension](https://ngxtension.netlify.app/utilities/operators/filter-nil/?ref=angularspace.com) library, that I used for some time and I do recommend checking it out. The idea is that we want to filter out `undefined` and `null` values from a pipe chain. ```TS export function filterNil(): MonoTypeOperatorFunction { return (source) => source.pipe( filter((d) => d !== null && d !== undefined) ); } ``` ### 2.) Data Polling There are situations where you loaded data from an API, cached it, but you wanted to poll the server in some time interval, thought the lifetime of the application, to refresh the data - weather app, stock prices, message polling etc. What I found out was, that it was pretty easy to implement such an operator: ```TS export function dataPolling(data: { loader: () => Observable; reloadSeconds: number; }): MonoTypeOperatorFunction { return (source) => source.pipe( switchMap(() => timer(0, data.reloadSeconds * 1000).pipe( switchMap(data.loader) ) ) ); } ``` ```TS // Component private http = inject(HttpClient); constructor() { const api = '...' this.http.get(api) .pipe( dataPolling({ reloadSeconds: 10, loader: () => this.http.get(api), }), ).subscribe((x) => console.log(x)); } ``` ### 3.) Complex Error Handling I know I said I won’t do examples with logging, as you can find more than enough of it, however I think it is worth mentioning that when you are doing some more complex logic inside the `catchError()` operator, it makes sense to create a custom operator for it. Here is an example of reporting the logs into Sentry and also notifying the user when something goes wrong. ```TS export function handleError( returnValue: K, errorMessage = "Server Error" ): OperatorFunction { const sentry = inject(SentryService); const notification = inject(NotificationService); return (source) => source.pipe( catchError((err) => { // log error in sentry sentry.log(error, 'error') // notify the user notification.notifyUser(errorMessage, 'error') // return something in the pipe chain return of(returnValue); }) ); } ``` ### 4.) Remember History Have you ever had a situation that you wanted to create an “undo change” button? Maybe caching values that arrived via a Websocket connection, or remember last N form edits by the user? One simple version for caching data can be done as follows: ```TS // defining an interface for the function // current and crevious values can be different, therefore T & K type RememberMemory = { // previous value that was cachced previous: null | K; // currently cached value current: T; // previous N cached values historyChain: unknown[]; }; ``` ```TS export function rememberHistory( // how many last N values to remember memory = 3 ): OperatorFunction> { return (source) => source.pipe( scan( (acc, curr) => ({ current: curr, previous: acc.current, // remember last N values historyChain: [...acc.historyChain.slice(-(memory - 1)), curr], }), { current: null as T, // ignore initial null previous: null, historyChain: [], } as RememberMemory ) ); } ``` To demonstrate how this pipe works, let’s say you have a stream of data and you want to cache them. When you have the following code, it will result to the below displayed picture. You can also change the logic if you want to change the order in the `historyChain` to keep the latest data on the first and not on the last position in the array. ```TS from([ { id: 1, name: "test1" }, { id: 2, name: "test2" }, { id: 3, name: "test3" }, { id: 4, name: "test4" }, ]).pipe( rememberHistory(4) ) ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/rxjs-pipe-remember-history-log.png) Custom Pipe Remember History Log ### 5.) Object Value Change When working with forms, you might listen to the form's `valueChanges` and only perform specific computations when the form is actually updated. The goal is to ignore intermediate changes, like typing or deleting characters, and only respond to meaningful updates. To solve this you could reach out for `distinctUntilChanged()` with some predicate and write it as: ```TS this.myForm.valueChanges.pipe( debounceTime(800), distinctUntilChanged( (prev, curr) => JSON.stringify(prev) === JSON.stringify(curr) ), map(d => /* do something */ ) ) ``` Using the stringifycation predicate, you are creating a guard to proceed only when the form was updated from it’s previous state. This predicate performs a deep comparison, which also includes checking form arrays and nested form groups. But what if you need this logic, but you wish to avoid using `JSON.stringify` ? You’d want a way to compare the previous and current object states, emitting only when they differ. Restricting the usage of `distinctUntilChanged()` and write out a custom logic to compare object changes can be done as follows: ```TS /** * creates a deep comparison between previous and current state * and emits only when object values changed (even for nested keys) */ export function objectValueChanged( config: { debounceTime: number; } = { debounceTime: 500, } ): MonoTypeOperatorFunction> { const isObjectValueChange = ( prev: K, curr: K ): boolean => { return Object.keys(curr).some((key) => { // value can be anything - string, number, object, etc. const previousValue = prev[key as keyof K] as any; const currentValue = curr[key as keyof K] as any; // if value is object - check child key and value changes if (currentValue instanceof Object) { return isObjectValueChange(previousValue, currentValue); } return previousValue !== currentValue; }); }; return (source) => source.pipe( // start with empty object make first emit startWith({}), // wait for user's input typing debounceTime(config.debounceTime), // use previous and current object values pairwise(), // only filter changed values for the object filter(([prev, curr]) => isObjectValueChange(prev, curr)), // return current object map(([_, curr]) => curr) ); } ``` ### 6.) Object Field Changes Speaking about forms, a more practical use case could be listening to a form and determining which fields have been changed / edited. The goal is to pass the initial state of the form (or object) into the pipe and get two outputs: an array of changed keys and an object where the keys are form field names and the values are booleans indicating if the field was edited. This pipe is indeed more complex, you don’t need to understand it in depth. Below I demonstrate how it works when attaching it on a form. ```TS type Booleanify = { [K in keyof T]: T[K] extends object ? Booleanify : boolean; }; export function objectChangedFields( initial: T ): OperatorFunction< Partial, { viewArray: string[]; viewObject: Booleanify; } > { // identify which fields have changed between two states. const getChangedFields = >( initial: K, current: Partial, currentKey = "" ): string[] => { let changedKeys: string[] = []; for (const key of Object.keys(current)) { // value can be anything - string, number, object, etc. const previousValue = initial[key as keyof K] as any; const currentValue = current[key as keyof K] as any; // create key to save const newKey = currentKey !== "" ? `${currentKey}.${key}` : key; // if value is object - check child key and value changes if (currentValue instanceof Object) { // save nested path changedKeys.push( ...getChangedFields(previousValue, currentValue, newKey) ); // go to next key continue; } // string or number comparison if (previousValue !== currentValue) { changedKeys.push(newKey); } } // return saved keys return changedKeys; }; // identify which fields have changed between two states. const getChangedFieldsObject = ( initial: Partial, current: Partial, cachedObj = {} ): Booleanify => { for (const key of Object.keys(initial)) { // value can be anything - string, number, object, etc. const previousValue = initial[key as keyof K] as any; const currentValue = current[key as keyof K] as any; // if value is object - check child key and value changes if (currentValue instanceof Object) { // create nested object (cachedObj as any)[key] = {}; // access nested cache const nestedCache = (cachedObj as any)[key]; // check if nested key changed getChangedFieldsObject(previousValue, currentValue, nestedCache); // go to next key continue; } // string or number comparison (cachedObj as any)[key] = previousValue !== currentValue; } return cachedObj as Booleanify; }; return (source) => source.pipe( map((data) => ({ viewArray: getChangedFields(initial, data), viewObject: getChangedFieldsObject(initial, data), })) ); } ``` I won’t dive into the details of how it works, there are some `any` castings and probably if you spend enough time on this, you could make it cleaner, but I’ve linked the GitHub repository at the end for those interested. What’s important is that you pass the initial state of the object (form) as `initial: T` to the function. The two functions, `getChangedFields()` returns an array of keys which were affected, and `getChangedFieldsObject()` returns an object with boolean values if the key was. changed. Now, let’s see this pipe in action with the following component: ```TS export class ExampleFormComponent { private readonly builder = inject(FormBuilder); myForm = this.builder.nonNullable.group({ name: [""], email: [""], age: [""], address: this.builder.group({ city: [""], }), items: this.builder.array([ this.builder.group({ name: [""], amount: [0], }), ]), }); constructor() { this.myForm.valueChanges.pipe( objectChangedFields(this.myForm.value) ).subscribe((fieldChange) => { console.log("Changed fieldsa:", fieldChange); }); } } ``` I am passing the `this.myForm.value` initial state to the custom pipe, but this can be any object to which I want to compare the pipe chain data into. For instance, if the `name`, `address.city` and `items[0].name` fields will be changed, the `objectChangedFields()` will return the following: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/rxjs-pipe-object-changed-fields-log.png) Custom Pipe Object Changed Fields Log ### 7.) Loading Stats In November 2024 I published an article titled [Creating Custom rxResource API With Observables](https://dev.to/this-is-angular/creating-custom-rxresource-api-with-observables-2abm?ref=angularspace.com), where I attempted to recreate a custom `rxResource()` API that works with Observables instead of signals. The main benefit of this custom API is that, even when working with Observables, you still get access to the loading state of the HTTP request. What I also realized is that I can take this logic and create a custom pipe that includes these HTTP status indicators. ```TS type RxResourceResult = { state: "loading" | "loaded" | "error"; isLoading: boolean; data: T | null; error?: unknown; }; ``` ```TS /** * used mainly for API requests to include HTTP state & data * about the HTTP call. Similar to rxResource */ export function loadingStatus(): OperatorFunction> { return (source) => source.pipe( map((result) => ({ state: "loaded" as const, data: result, })), // setup loading state startWith({ state: "loading" as const, data: null, }), // handle error state catchError((error) => of({ state: "error" as const, error, data: null, }) ), // map the result to the expected type map( (result) => ({ ...result, isLoading: result.state === "loading", } satisfies RxResourceResult) ) ); } ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/rxjs-pipe-loading-state-log-1.png) Custom Pipe Loading State Log ## Summary RxJs already provides bunch of operators to manipulate data in the pipe chain, however, occasionally it happens that you need to create a custom, more specific pipe. From this article, you learned that operators are just simple functions that return either `OperatorFunction` when the input and output types are different, or `MonoTypeOperatorFunction` when they stay the same type. You also have seen a few examples where custom operators could be useful. Personally, I find the `objectChangedFields()` and `loadingStatus()` operators to be the most useful. I hope you liked the article. You can find all the above mentioned, and more custom pipe on the following [Github repository](https://github.com/krivanek06/stackblitz-rxjs-playground/tree/main/src/custom-rxjs-operators?ref=angularspace.com). Feel free to share your thoughts, and connect with me on [dev.to](https://dev.to/krivanek06?ref=angularspace.com) | [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2024-11-13-at-15.52.00--1---4-.jpg) ### Boost Your App's Performance with NgOptimizedImage URL: https://www.angularspace.com/boost-your-apps-performance-with-ngoptimizedimage/ Last updated: 2025-01-22T10:28:54.000Z If you've worked on a web app, you know images can be a big deal for design and performance. While they make pages look great, they can also slow everything down. That’s where Angular’s `NgOptimizedImage` directive comes in. It’s a handy tool for optimizing images, so they load faster without losing quality. Let’s dive into how this directive can make your Angular app snappier and improve the user experience. --- ## Why Optimize Images Anyway? We all want our apps to look good, but images come with a cost - longer load times. Optimizing images can dramatically improve performance metrics, especially the [Largest Contentful Paint (LCP)](https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint?ref=angularspace.com), which measures how fast the largest piece of content appears on the screen. This is huge for user experience and SEO, and Angular’s `NgOptimizedImage` directive makes it easy to get it right without manual tweaks. --- ## Getting Started The first step is to import the `NgOptimizedImage` directive from the `@angular/common` module. You can do this in your standalone component or module file: ```typescript import { NgOptimizedImage } from '@angular/common'; @Component({ // ... imports: [ NgOptimizedImage, // ... other imports ], // ... }) export class DemoComponent {} ``` Once imported, you can use the key image optimization features provided by `NgOptimizedImage`. ## Key Features of `NgOptimizedImage` ### 1\. Responsive Images with `srcSet` and `sizes` The `srcSet` and `sizes` attributes allow the browser to load the right image size for each device, which is especially helpful for performance. The great thing about Angular’s `NgOptimizedImage` directive is that it generates the `srcSet` for you, so you don’t need to do it manually. **Example:** ```html ``` Here, `sizes="80vw"` sets the image to 80% of the viewport width. Angular will handle the rest, creating a `srcSet` with various resolutions so that each device gets the best fit. This saves time and ensures a smooth load no matter the screen size. ### 2\. Customizing Image Breakpoints If you’re working with specific screen sizes, Angular lets you customize breakpoints by setting an `IMAGE_CONFIG` token. This way, you can pick just the breakpoints that match your app’s design. **Example:** ```typescript providers: [{ provide: IMAGE_CONFIG, useValue: { breakpoints: [384, 640, 750] } }] ``` Now Angular will only generate images for 384px, 640px, and 750px, which keeps your file sizes smaller and your app faster. --- ### 3\. Prioritizing Key Images with `priority` Ever noticed how long it can take to load a big image like a banner? The `priority` attribute tells Angular which images are critical for loading first. If you set `priority`, Angular preloads the image, making sure it appears quickly, improving that all-important LCP. On top of that, it assigns the image a `fetchpriority=high`, signaling its importance, and sets the loading behavior to `eager` to make sure there's no unnecessary delay in displaying it **Example:** ```html ``` For server-side rendered apps, Angular even adds a `` tag for these images, so they start loading right away. This is perfect for hero images or banners that you want users to see immediately. --- ### 4\. Fill Mode for Flexible Layouts Need an image to fill a container, like a background? `fill` mode has you covered. It lets an image scale to fit the container without needing fixed dimensions, which is perfect for layouts with unknown widths or heights. **Example:** ```html ``` Just make sure your container has `position: relative`, `absolute`, or `fixed`, so the image fills it properly. This feature is great for responsive layouts where you want images to adapt smoothly. --- ### 5\. Lazy Loading for Off-Screen Images Angular makes lazy loading effortless with `loading="lazy"`, which defers loading images until they’re about to be visible. This reduces the initial page load and is awesome for things like image galleries. **Example:** ```html ``` Be cautious with images that are crucial to the layout. Avoid using lazy loading for LCP images, such as a logo. This ensures they load immediately and maintain a smooth user experience. --- ### 6\. Placeholders for a Better Loading Experience Nothing kills user experience like a blank loading image. With `placeholder`, you can show a temporary low-res version or even a Base64-encoded preview while the main image loads. This provides a smoother experience as users see a preview of the image right away. **Example:** ```html ``` If you'd like to use a Base64-encoded placeholder, you can easily generate the string using: - Online Tools: Use a free online Base64 converter like [Base64-Image](https://www.base64-image.de/?ref=angularspace.com), and it will generate the encoded string. - Command Line Tools: Open a terminal and type the following command: ```bash base64 -i input-image.jpg ``` Once you have the Base64 string, you can embed it directly into your HTML as the src for the placeholder: ```html ``` This approach makes the user experience smoother and creates a seamless visual flow. If you use a CDN, you can [generate low-res placeholders](https://cloudinary.com/blog/guest%5Fpost/create-image-loading-placeholders-in-nuxtjs?ref=angularspace.com) automatically. Just keep them small (under 4 KB) to avoid slowing down the load. Angular will even throw an error if the placeholder size is too big, which is a good reminder to keep things light: ``` NG02965: The NgOptimizedImage directive (activated on an element with the ngSrc="angular.jpg") has detected that the placeholder attribute is set to a data URL which is longer than 4000 characters. This is discouraged, as large inline placeholders directly increase the bundle size of Angular and hurt page load performance. For better loading performance, generate a smaller data URL placeholder. ``` --- ## CDN Integration for Faster Image Delivery By using a Content Delivery Network (CDN) with `NgOptimizedImage`, you get faster load times because images are served from servers closer to your users. CDNs cache content across the globe, meaning shorter load times and less data travel, which makes a huge difference, especially for users far from your primary server. CDNs also often compress images, ensuring the best quality at the smallest size, which boosts performance even further. When combined with `NgOptimizedImage`, CDNs can help you build a fast, responsive app that works well for users everywhere. --- ## A Practical Example Now, let's look at a real-world example to see how the `NgOptimizedImage` directive can make a big difference in performance. Imagine we’re building a gaming site that displays game cover images. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/demo-app-1.png) Here’s the initial setup with the standard `src` attribute: ```html Assassin's Creed Game Cover Art
@for (image of list; track $index) { Thumbnails Game Gallery }
``` When we analyze the page in Lighthouse, the First Contentful Paint (FCP) comes in at 1.4 seconds, while the Largest Contentful Paint (LCP) lags behind at 13.5 seconds, which is not ideal. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/demo-app-initial-lighthouse-score-3.png) By making a few tweaks with `NgOptimizedImage`, we can get those numbers way down: ```html Assassin's Creed Game Cover Art
@for (image of list; track $index) { Thumbnails Game Gallery }
``` > \*\* **Important Note:** Setting the priority for multiple images displayed in a loop isn’t always the best approach, specially when some of them aren’t immediately visible in the viewport. To optimize performance, it’s better to assign fetchpriority="high" only to the visible images, while using loading="lazy" for those outside the viewport. This ensures efficient loading and better resource management. To go the extra mile, we add a `preconnect` link for our image server in the ``: ```html ``` And in our app configuration, we set up a [custom image loader](https://angular.dev/guide/image-optimization?ref=angularspace.com#configuring-an-image-loader-for-ngoptimizedimage) for our CDN (e.g., `Imgix`): ```typescript const appConfig: ApplicationConfig = { providers: [provideImgixLoader('https://via.assets.so/')], }; ``` With these small adjustments, we see a huge performance boost—FCP drops to just 0.2 seconds, and LCP is down to a quick 0.7 seconds! This is a perfect example of how `NgOptimizedImage` can make a real difference in your app’s speed and user experience. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/demo-app-final-lighthouse-score.png) --- ## Wrapping Up Angular’s `NgOptimizedImage` is a fantastic tool for improving image handling and app performance. Whether you’re working with responsive images, lazy loading, or CDN integration, this directive makes it easy to get things right. With these optimizations, you’ll have a faster, smoother app that keeps users happy. Give `NgOptimizedImage` a try and see the difference in your next project! --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2024-11-13-at-15.52.00--1---3-.jpg) ### From a tax officer to a Lead Software Engineer URL: https://www.angularspace.com/from-a-tax-officer-to-a-lead-software-engineer/ Last updated: 2025-01-16T07:47:50.000Z ## **Part 1\. How to learn c# in one month** 🤣👉 ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Learn-C--in-one-month-1.png) I was that kid in school - the one who always had her homework done, sat in the front row, and made teachers smile. I finished school with the highest marks in every subject. But here’s the funny part: I didn’t even have a computer back then. At university, I dove into finance and graduated with a Master’s degree in taxation. The highest marks there, too. (Yes, I was a bit of an overachiever.) But still, programming was nowhere on my radar. Then came my first job: four long years at the tax office. And let me tell you - it was a disaster. I hated it. Mondays were my nemesis. I remember thinking, “Is this really it? Am I going to spend my life filing papers and chasing unpaid taxes?” Eventually, I hit my breaking point. I told myself, “Listen, you need to do something with your life.” And that’s when I discovered programming courses. JavaScript, HTML, CSS - the beginner’s bundle. This was 10 years ago, and I was still working full-time. So, my evenings were spent at the courses three times a week for three hours each. By day, I was crunching numbers at the tax office, and by night, I was crunching through code. I didn’t have free time - I had JavaScript syntax errors. When I finished the courses, I quickly realized that “Hello, World!” and a basic website weren’t enough to get a job. So, I started looking for internships. I found one at an IT company for students willing to learn C#. Yes, it was not JavaScript! Now, let me be clear - I knew nothing about C# 🤯. I didn’t even know what the "C" stood for. But in true “fake it till you make it” fashion, I gave myself a crash course. For one month, it was just me, online tutorials, and enough coffee to fuel a small city. The goal? Learn just enough C# to pass the interview. And guess what? I got in! Six months later, the company asked if I wanted to switch to front-end development because they needed Angular developers. I thought, “Sure, why not? What’s one more "small" shift at this point?” I learned Angular, joined an internal project, and officially became a junior front-end developer. That was my first real IT job, and I felt like I had just hacked the system - literally and figuratively. And that’s how my programming journey began - with zero experience, a lot of late nights, and a pinch of chaos. At this point, I realized something important: switching careers while managing a full-time job (and life in general) wasn’t about having time - it was about making time. A lot of people say, “I don’t have time to learn a new profession.” Trust me, I get it. But the truth is, focusing on the result and prioritizing your goals can make a world of difference. ### Here’s what worked for me (and might work for you): - ✅️ **I surrounded myself with learning opportunities.** I found YouTube lessons I could watch on the bus to work. I stopped binge-watching TV shows (sorry, Netflix) and focused on doing my homework instead. I made learning unavoidable, like that annoying alarm you can’t snooze. - ✅️ **I told my friends and family about my goal.** This was my way of saying, “Hold me accountable!” Plus, every time they asked, “How’s the coding going?” I felt a little more motivated to keep pushing. - ✅️ **I mixed it up.** Watching videos during commutes, practicing during quiet times at work, and attending courses after hours - it was like a coding bootcamp I designed myself. - ✅️ **I said “yes” to everything.** During my internship, I tried being a Scrum Master, Business Analyst, QA, and even dove in automation. Every time someone said, “Can you try this?” my answer was, “Sure, why not?” Did I know what I was doing? Not always. But I figured it out. Now, let’s not sugarcoat this. I made some big mistakes too: - 📛 **I forgot to rest.** I pushed myself so hard that I burned out, developed health issues, and ultimately went through a divorce with my first husband. Lesson learned: chasing your goals is important, but so is taking care of yourself and your relationships. - 📛 **I doubted myself constantly.** I overthought everything and missed opportunities because I didn’t feel “ready.” Spoiler: you’re never 100% ready. Do it anyway. --- ## **Part 2\. The hardest part: overcoming imposter syndrome** 😰 ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Real-programmer.png) When I got my first real IT job, I was 27\. Sounds cool, right? Well, not exactly. My colleagues were mostly 19- or 21-year-olds, fresh out of university with programming degrees. Meanwhile, there I was - the “ancient” newbie with a background in taxes. I kept thinking, “How can I ever be on the same level as them? They’re going to fire me any day now. I must be the dumbest person in the room.” I wasn’t, but it felt like it at the time. The stress was real, and so was my new best friend - chocolate. I gained 10+ kilos eating sweets to deal with the anxiety. (On the bright side, nobody complained when I brought cake 🍰 to the office.) But here’s the thing: even though I felt like a fraud most of the time, I didn’t let that stop me. I was constantly learning, constantly practicing. ### During this time, I learned a few important lessons that I still live by: - ✅️ **Don’t let fear stop you from learning and practicing.** It’s okay to feel like you don’t know enough - just keep going. - ✅️ **Don’t be afraid to ask questions.** Seriously, no one will bite you. - ✅️ **Respect your colleagues’ time** \- Google it first, or at least try to figure it out on your own. Don’t make them your personal “Google assistant.” And hey, that’s what ChatGPT is for, right? - ✅️ **If you don’t understand the answer, write it down and Google it later** \- or ask AI to simplify it. I did this all the time. I had some pretty strange notes after talking to my tech lead - honestly, they looked like the ramblings of a mad 👨‍🔬 scientist. But hey, it worked! Looking back, those early days weren’t just hard - they were transformative. I didn’t just learn Angular or how to debug. I learned how to face my fears and keep moving forward. --- ## **Part 3: I need to work for a big company to prove I’m a real programmer** 🧑‍💻 ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/English-humor.png) After about two years as a developer, I started dreaming about joining a big tech company. No, I’m not talking about the IT giants (I wasn’t that ambitious… yet). But I did want to work for one of the biggest companies in my country. So, I started preparing. I spent hours studying for interviews and, well… failing them. A lot. But eventually, persistence paid off, and I got an offer. That’s when the real adulting began. My new project wasn’t just about writing code. Oh no! I had to work directly with the customer, and guess what? There was no team in my time zone. It was just me, Zoom calls, and a whole lot of “English only” hardcore mode. My English? Let’s just say it wasn’t exactly polished back then. To make things even more interesting, my team lead was from London. He had this dry, British humor that I didn’t understand at all. I spent the first few weeks worrying that they’d fire me because I couldn’t make a clever joke in English. Thankfully, it turns out writing good code is more important than cracking good jokes. This phase of my career wasn’t just about becoming a better developer - it was about learning to communicate, negotiate, and push my English skills to almost an advanced level. It wasn’t easy, but it was absolutely worth it. ### Here’s what I learned as a middle developer during this time: - ✅️ **Great communication skills can save you in any situation.** If you can explain your ideas clearly, it’s like half the job is already done. Plus, you’ll avoid those awkward “Wait, what do you mean?” moments. - ✅️ **Don’t stop learning.** You might feel like you’ve got it figured out, but trust me - you’ve only scratched the surface. There’s always more to learn, and you’ll thank yourself later for putting in the effort. - ✅️ **Find a mentor.** A good mentor isn’t just someone who answers your questions - they’re the person who shows you things you didn’t even know you needed to ask about. This chapter of my career taught me that being a programmer is about more than just code. It’s about learning how to connect with people, adapt to challenges, and grow as a professional. --- ## **Part 4: I am your mother, Luke 😉**👀 ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Yoda.png) Okay, so there was no Luke, but there was a little Tim - my son. This is the beginning of my current journey. Almost 4.5 years ago, I became a mom, and everything changed. After a six-month maternity break, I decided to change the company. I prepared (while also figuring out how to survive on three hours of sleep) and landed an offer at the company I’m still with today. This is where my journey toward becoming a senior developer - and now a leader - truly began. It hasn’t been all smooth sailing. This job has given me incredible highs and some serious lows. I’ve been thrilled by my progress and totally burned out at times. But it’s also where I grew the most. ### Here’s what I’ve accomplished so far: - ✅️ I started mentoring others, guiding juniors, and loving every moment of it. - ✅️ I became an interviewer and discovered that helping hire the right people is one of my favorite parts of the job. - ✅️ I overcame my fear of public speaking (a phobia I had since school) and now genuinely enjoy giving talks. - ✅️ I created courses, contributed as an author, and helped others grow as a mentor. Right now, I’m at another turning point - transitioning from a senior developer to a lead role. And let me tell you, it’s not just about a new title. It’s about completely rewiring how I think and work. ## **The big challenge** Here’s the thing: what made me a good developer doesn’t necessarily make me a good leader. As a developer, I could zone in, focus on technical problems, and write clean code like it was my superpower. But now? That’s just one piece of the puzzle. I’ve realized I need to stop focusing so much on the code and start focusing on people. (Spoiler: people are way more complex than JavaScript bugs 🤯🐛!) My priorities now include: - ✅️ **Learning how to negotiate with people.** Turns out, you can’t debug humans like you do code. Unfortunately.. - ✅️ **Thinking strategically.** It’s no longer about “perfect code.” Now, it’s about asking, “Will this make the business happy?” And let’s be honest, happy businesses pay the bills. - ✅️ **Delegating.** My old mantra was, “If you want something done right, do it yourself.” Turns out, that’s a one-way ticket to burnout. Now, I’m learning to trust others - even if I secretly want to grab the keyboard sometimes. Honestly, it feels like I’m tearing down my old foundation and building something entirely new. It’s scary, but also really exciting. --- ## **My goal for this year** This year, I’ve set one big goal: to spend more time with people than with lines of code on my screen. I want to shift from being the “go-to coder” to being a mentor, a coach, and someone who can see the bigger picture. Oh, and about three months ago, I also started actively sharing my journey on LinkedIn. It’s been an amazing ride so far, and I’m proud of the progress I’ve made in building my personal brand and connecting with others. ### Here’s what I’ve learned during this chapter of my life: - ✅️ **Time management is all about priorities.** When you’re balancing a child, a full-time job, and life in a foreign country with no extra help, you quickly learn to use every minute wisely. - ✅️ **Development is not just about writing code.** It’s about the people who write it. This is a truth every developer needs to learn. - ✅️ **Don’t be afraid to ask for help.** Be open - you’d be surprised how much support you can get when you need it. - ✅️ **Listen more than you talk.** Growth comes from both knowledge and understanding others. And finally, let me leave you with this thought: you have to believe in what you do. Even when it’s hard, even when you doubt yourself , just keep moving forward. This is my story so far, but the journey isn’t over. Who knows what’s next? ### 12x e-Book Giveaway! - Mastering Angular Test-Driven Development URL: https://www.angularspace.com/12x-e-book-giveaway-mastering-angular-test-driven-development/ Last updated: 2025-01-15T10:35:46.000Z ## First 2025 Angular Space Giveaway! 😄 Giveaway with good old fashioned Discord Raffle! [**Ezéchiel Amen AGBLA**](https://www.linkedin.com/in/e-amen-agbla/?ref=angularspace.com) \- has released a fantastic e-Book last year about Test Driven Development! [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2025-01-15-at-11.30.47.png)](https://www.linkedin.com/in/e-amen-agbla/?ref=angularspace.com) I have 12x to giveaway thanks to **PACKT** & **Ezéchiel Amen AGBLA**! ### Who is this book for? This book is for both experienced Angular developers and junior developers. Tech leads and architects who are responsible for code quality and scalability will also benefit from this book, as well as software development students looking to learn TDD concepts. Whether you're an experienced developer, a junior programmer, or a student, this book will equip you with the necessary knowledge to implement TDD in Angular projects. ### More details about the book here: [https://www.packtpub.com/en-us/product/mastering-angular-test-driven-development-9781805126089?utm\_medium=affiliate&utm\_campaign=4aaca297-a549-04f9-0734-65457a2dde96&utm\_term=e2025f61-d111-eb11-a812-00224801bc77&utm\_content=B21146](https://www.packtpub.com/en-us/product/mastering-angular-test-driven-development-9781805126089?utm%5Fmedium=affiliate&utm%5Fcampaign=4aaca297-a549-04f9-0734-65457a2dde96&utm%5Fterm=e2025f61-d111-eb11-a812-00224801bc77&utm%5Fcontent=B21146&ref=angularspace.com) Instructions on how to participate in giveaway below 👇 _This post is for subscribers only._ ### Built RxJS Visualizer in 4 Hours with AI - No Coding! URL: https://www.angularspace.com/built-rxjs-visualizer-in-4-hours-with-ai-no-coding/ Last updated: 2025-01-28T23:08:16.000Z I made RxJS Visualizer in 4h using AI without writing any code manually. Here is my experience. It was both pretty & ugly. ## RxJS Marble Diagram visualizer 0:00 /0:56 1× RxJS Marble Diagram visualizer is something AI wasn’t trained on very much since there are maybe 3-4 similar apps in existence right now. None of these apps aimed to make existing Marble Diagrams alive 1:1 Goal was to take Marble Diagram and make it animated to present the data flow OVER TIME. While retaining 100% of the UI from original. This kind of app is something new and AI struggled A LOT. First, I asked ChatGPT to generate a prompt best fitted for another AI that can generate entire applications. I explained to GPT what I wanted and got a very good prompt. Long detailed prompt. To build the app with that prompt, I used [bolt.new](http://bolt.new/?ref=angularspace.com). ### Using Angular did not work out well... My first prototype with Angular was a total disaster. The marbles were square, they didn’t flow at all... it was chaos. Sure, with hours of tinkering, I might have been able to guide Bolt to fix it, but I didn’t have that kind of time. On top of that, Angular-specific issues kept popping up, breaking things left and right. To make matters worse, Bolt kept flipping between Angular versions 17 and 18, creating an even bigger mess. ### Bolt did better with React I used the same prompt to generate the Visualizer with React, and the results were superb. It immediately looked much closer to what I envisioned. The animations worked, though there were some quirks - like marbles flowing off-screen or not staying on the line. There were many small issues: - It lacked controls - Bolt Added a weird play button. - There was no "add values" button. - It wasn’t behaving like RxJS. - Buttons had to be changed. - Button placements needed adjustments. - An actual RxJS logic representation for each operator had to be added. A huge step forward compared to Angular. However fixing one thing often broke another, creating a frustrating cycle of endless adjustments. ## Bolt went DRY… in a bad way ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2025-01-07-at-19.54.25-1.png) Bolt kept creating new files, leaving old ones in the same place, and later adding changes to the wrong files. On top of that, it kept redoing already working pieces of logic without my permission, breaking one thing after another. A mess... Here we are at the point where a non-engineer would never be able to finish this app. Maybe even a mid-tier engineer would struggle with prompting the AI. To really move forward without doing the actual code myself. I had to do what an experienced engineer would do. I looked at the codebase, and it was clear: - AI ran itself into a corner by going DRY. 😏 Additional mess came from leaving too many unused files and getting lost in them. I directed Bolt to fix the correct files but kept breaking existing logic. DRY was the main issue - it had crammed majority of core logic into single files. - Single timeline file - Single operator file, - Single label file, - Single data store file, etc. When I saw this, I asked AI to immediately break it apart, duplicate code, and create separate files for each operator. It took some time to make Bolt understand this task and required some manual checking of the file tree. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2025-01-07-at-19.53.10-1.png) After a couple of iterations, WE got it right. :) AI co-op was very nice with my experience added into the mix. I tried very hard not to touch any line of code manually to see if Bolt can handle this. ### Finally a great progress! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2025-01-07-at-19.53.23-1.png) With separated files for each operator, I started adding screenshots of existing marble diagrams and descriptions from RXJS documentation. AI got this right almost on the first shot after all codebase optimizations have been made. I still had to adjust small but crucial details like: - `combineLatest()` will start emitting only when every given observable has emitted at least once. This way, I completed the initial set of operators for version 1. ### Desktop only for now... Responsiveness isn’t supported yet, that's mainly because of my initial prompt, which didn’t include a request for a responsive design. Totally on me. 😅 When I finally asked Bolt to add responsiveness, it completely broke everything. So, for now, v1 is desktop-only. Fixing this will likely need some good old manual intervention down the line. ## AI is not replacing you today or in next couple of years To wrap up: - Creating something with specific, detailed requirements still requires solid engineering knowledge. - I couldn't have built this app from scratch in 4 hours without AI. - AI serves as an excellent prototyping tool, laying down a workable foundation. - It will speed up your work massively, potentially reducing head count in your team In the near future I expect to see more smaller teams moving faster then before. Reducing headcount per team, doesn't mean reducing the amount of position overall. Thanks to this more companies will be able to compete on the market with smaller budgets. Competition is always good. Working with Bolt today is like having a junior/mid-level engineer under your wing, an experience that felt like constant code/product review. Can’t wait to see where we land by the end of 2025. ### My personal advice to you. Whatever comes next, make yourself future-proof by mastering high-level architectural skills, diving deep into the nuances AI often overlooks, and sharpening your leadership abilities. In the next 5 years, we might see developers evolving into leaders of AI-driven teams, orchestrating the process and ensuring the pieces come together seamlessly. The role might shift toward fine-tuning details, guiding AI through the complexities, and gaining a stronger grasp of the business side to bridge the gap between tech, business & strategy. ## Try it out!! If you want to test the Visualizer yourself - try out the Beta version now 😄, link 👇. I would appreciate feedback in comments section 🙂 [Vite + React + TS![](https://static.ghost.org/v5.0.0/images/link-icon.svg)](https://www.rxvisualizer.com/?ref=angularspace.com) I'm planning to open source it soon. Stay tuned! ### Dynamic Service Instantiation in Angular URL: https://www.angularspace.com/dynamic-service-instantiation-in-angular-2/ Last updated: 2025-01-10T08:53:06.000Z Recently, I came across the following [dynamic service instantiation in Angular](https://www.linkedin.com/posts/roberto-heckers-2313453b%5Fangular-activity-7274362560110379008-fFuj?ref=angularspace.com) from [Roberto Heckers](https://www.linkedin.com/in/roberto-heckers-2313453b/?ref=angularspace.com), where he generates a service based on some condition. Occasionally, we bump into a use case where you have multiple services, each of them sharing the same method name with a different implementation, and you want to call only one of these services based on some condition. In the following article, I decided to create a similar example and provide more use cases where dynamic service generation may be useful. ## Application Overview Let’s have an example of a money-sending application (Wise, Paysend, etc.). You want to send some amount to your friend. You are required to put the amount, the address, and select what kind of payment option you want to use. Here is the ugly version of the UI and the HTML and TS part of the form. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/application-overview.png) Application UI Overview ```html Amount Address Paypal Stripe Venmo ``` ```TS // TS form creation readonly form = new FormGroup({ amount: new FormControl(0, { nonNullable: true, validators: [Validators.required, Validators.min(0)], }), address: new FormControl('', [Validators.required]), type: new FormControl<'paypal' | 'stripe' | 'venmo'>('paypal', { nonNullable: true, validators: [Validators.required], }), }); ``` Nothing complicated so far. What we will be curious about is choosing the right service based on the selected option with the radio buttons. ## Multiple Service Providers We have 3 radio buttons - Paypal, Stripe & Venmo. Each option represents one of the following services: ```TS @Injectable({ providedIn: 'root' }) export class PaypalService extends PaymentBaseService { override pay() { // logic for payment } } @Injectable({ providedIn: 'root' }) export class StripeService extends PaymentBaseService { override pay() { // logic for payment } } @Injectable({ providedIn: 'root' }) export class VenmoService extends PaymentBaseService { override pay() { // logic for payment } } ``` There are 3 services (`PaypalService`, `StripeService` and `VenmoService`) which extend the `PaymentBaseService` that holds some common data for all of them. Keep in mind that in real life, all these 3 payment services may bring some 3rd party library, where all of them try to connect with the provider (Stripe, Venmo, etc.) on the service instantiation. This connection establishment may be slow, it can increase the bundle size, have the potential for memory leaks, and can throw errors. Therefore we don’t want to eagerly create all these payment services, especially when only one will be needed. We want to defer the creation of only one payment service based on what option the user chooses when the form is submitted. Also, it is worth noting that if the service is only used in a lazy-loaded component that is part of a lazy-loaded route (which is our case), an instance of the service is created only when that route is navigated, and the service is first injected to the routed component. Meaning, that even if we have 3 services registered in `provideIn: 'root'`, the instance will be created only when the user navigates to the route where they are first injected. ## Dynamic Service Resolving As mentioned previously, we don’t want to eagerly create an instance of each payment service, as they may hold some large JS logic; instead, based on some conditions, we want to instantiate the correct one. For that, we can use Angular’s [Injector](https://angular.dev/api/core/Injector?ref=angularspace.com) and do something as follows: ```TS type PaymentType = 'paypal' | 'stripe' | 'venmo'; @Component({ selector: 'app-page-payment', imports: [ /* .... */ ], template: ``, standalone: true, }) export class PagePaymentComponent { readonly #injector = inject(Injector); readonly form = new FormGroup({ amount: new FormControl(0), address: new FormControl(''), type: new FormControl('paypal') }); onSubmit() { // get the payment type const type = this.form.controls.type.value; // update the payment service const paymentBaseService = this.updatePaymentService(type); // pay paymentBaseService.pay(); } /* dynamically initialize a service by the type */ private updatePaymentService(type: PaymentType) { switch (type) { case 'paypal': return this.injector.get(PaypalService); case 'stripe': return this.injector.get(StripeService); case 'venmo': return this.injector.get(VenmoService); default: throw new Error(`Unknown payment type: ${type}`); } } } ``` When the `onSubmit()` is executed, the `updatePaymentService()` method using the `injector` returns the correct service instance. We defer creating a service until the user chooses the type and submits the form. These payment services are created only once since they are singletons and live through the application's lifetime. ## Dynamic Service Use-Cases Dynamic service loading is probably not the first thing you have in mind when working on the feature. You only start to consider this option when the whole feature is a bit slow. Some real-life examples where this strategy could be useful are the following: - **Dynamic Formatter Service** \- A document or data set might need to be formatted differently (e.g., JSON, XML, or CSV) based on user export preferences. You may have multiple service, each dedicated for a specific data format. - **File Upload Handler** \- When uploading files, the service handling the upload may vary based on the file type, e.g., images, videos, or documents. You create one service per uploading type. Or you may have different storing options, such as S3 for files and something else for video content. - **Notification Service** \- A system might need to send notifications via different channels such as Email, SMS, or Push Notifications, based on user preferences or settings. You can go with services such as - `EmailNotificationService`, `SmsNotificationService`, or `PushNotificationService`. - **Authentication Provider** \- A system might need to authenticate users against different providers like Google, Facebook, or custom enterprise SSO. All of the above mentioned examples could the below showed pattern how to dynamically instantiate a service. ```TS @Injectable({ providedIn: 'root' }) export class AuthService { private injector = inject(Injector); private authProvider!: AuthBaseService; setAuthProvider(provider: 'google' | 'facebook' | 'enterprise') { switch (provider) { case 'google': this.authProvider = this.injector.get(GoogleAuthService); break; case 'facebook': this.authProvider = this.injector.get(FacebookAuthService); break; case 'enterprise': this.authProvider = this.injector.get(EnterpriseAuthService); break; } } login(credentials: any) { return this.authProvider.login(credentials); } } ``` ## Dynamic Service Benefits I can’t say that this will be my go-to strategy when injecting services into a component, however, this dynamic service creation still has some benefits compared to normal injection, which is: - **Integration with Third-Party Services** \- As the payment service example, each service could use some large 3rd party library, trying to establish a connection inside the constructor, and we want to avoid waiting for initialization with the 3rd party. If that service won’t be even needed, we can save time and resources. - **Runtime Decision Making** \- You may have some very dynamic applications, where a large amount of data is loaded from the user config (online Photoshop, Google Maps). You are already using dynamic components with the `@defer` syntax, but it can also happen that you want to distinguish which services will be created as the user may have a demo account or a paid membership account. - **Compliance and Customization** \- This goes back to the second point (runtime decision-making). For example with the payment service, you may need to create one shared payment service and then for each country create a region-specific payment gateway (service) which includes some local regulations. ## Summary In this short article, we went through how dynamic service instantiation works in Angular, and what are the benefits and use cases where it could be considered useful. The payment code mentioned in the article is available on [Github](https://github.com/krivanek06/stackblitz-dependency-injection/tree/main/src/payment-module?ref=angularspace.com). I hope you liked the article and feel free to share your thoughts, and connect with me on [dev.to](https://dev.to/krivanek06?ref=angularspace.com) | [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2024-11-13-at-15.52.00--1---2-.jpg) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/angular-university-banner-4--1--1.jpg)](https://angular-university.io/?ref=angularspace.com) ### How Angular keeps your UI in sync URL: https://www.angularspace.com/how-angular-keeps-your-ui-in-sync/ Last updated: 2025-01-08T11:35:20.000Z ## Understanding Change Detection Change detection in Angular is a key process that keeps the app's state and user interface in sync. It's how Angular makes sure our UI updates when the underlying data changes. Without it, any changes in your app wouldn't show up in the UI automatically, making the app inconsistent and unreliable. Change detection is important because it keeps the UI accurate, ensuring every user action, data fetch, or event is shown correctly, making the app responsive and user-friendly. --- ## How Change Detection Works in Angular The change detection process in Angular involves two main stages: 1. **Marking the Component as Dirty:** This initial stage occurs when an event that can alter the state of a component happens. For instance, a user clicks a button, which triggers Angular to mark the affected component as `dirty`. This marking indicates that the component requires a check-up for potential changes. 2. **Refreshing the View:** This is where `zone.js` comes into play. zone.js is a library that helps Angular track asynchronous operations like XHR requests, DOM events, and timers (e.g., `setInterval`, `setTimeout`). When an asynchronous event occurs, Angular captures it through the [onMicrotaskEmpty](https://github.com/angular/angular/blob/18.1.0/packages/core/src/change%5Fdetection/scheduling/ng%5Fzone%5Fscheduling.ts?ref=angularspace.com#L47-L60) Observable: ```typescript this._onMicrotaskEmptySubscription = this.zone.onMicrotaskEmpty.subscribe({ next: () => { if (this.changeDetectionScheduler.runningTick) { return; } this.zone.run(() => { this.applicationRef.tick(); }); }, }); ``` And [traverse the view tree](https://github.com/angular/angular/blob/18.1.0/packages/core/src/application/application%5Fref.ts?ref=angularspace.com#L585C9-L592C10) to detect and propagate any changes: ```typescript for (let {_lView, notifyErrorHandler} of this._views) { detectChangesInViewIfRequired( _lView, notifyErrorHandler, isFirstPass, this.zonelessEnabled, ); } ``` In Angular, a `view` refers to an instance of [ViewRef](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/view%5Fref.ts?ref=angularspace.com#L45). Think of `ViewRef` as a box that contains a bunch of important information about the component, such as the current state of inputs, the template, directives being used, and binding statuses. Each component has its own `ViewRef` box, making it easier for Angular to manage and update components during change detection. The `detectChangesInViewIfRequired` method triggers the [detectChangesInView](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/change%5Fdetection.ts?ref=angularspace.com#L458), which defines the criteria for checking components, inspecting various flags associated with the view and its mode, to determine if any updates are necessary. If the view needs refreshing, the [refreshView](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/change%5Fdetection.ts?ref=angularspace.com#L497) method is called. This method executes the [template function](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/shared.ts?ref=angularspace.com#L445) (which is a component template compiled into a regular JavaScript function) with the render flags and context to generate the component view and starts a chain reaction by triggering event detection for the view's child components through the [detectChangesInChildComponents](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/change%5Fdetection.ts?ref=angularspace.com#L305) method. So to perform change detection, Angular traverses the views' tree and executes template functions on each component. --- ## Default Change Detection In the beginning, most of us used Default change detection - it's like being in the middle of a bustling city – there's noise everywhere, distractions pulling your attention in every direction. This is how it was with Angular's default change detection. It keeps an eye on everything, even when nothing's happening. And sure, it works, but it's a bit overkill, causing performance hiccups, especially as your app grows. This strategy is the default change detection mechanism, and it’s applied automatically unless we explicitly override it with an OnPush strategy. It relies on the [CheckAlways](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/change%5Fdetection.ts?ref=angularspace.com#L464-L467) flag which means that Angular will run change detection for all components whenever any event occurs. ```typescript let shouldRefreshView: boolean = !!( mode === ChangeDetectionMode.Global && flags & LViewFlags.CheckAlways ); ``` So if this flag is set, then it calls the mentioned `refreshView` method (`shouldRefreshView` flag is set to `true`), ```typescript if (shouldRefreshView) { refreshView(tView, lView, tView.template, lView[CONTEXT]); } ``` which refreshes not just the current view but also all its child views, if there are any. This ensures everything stays consistent, but it can be inefficient for larger apps because it might do more checks than needed. ![Click on Component D, which triggers an animation throughout the entire DOM tree from top bo the bottom](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68fmdqi4mfyfs8t204ul.gif) So, it's kind of like a domino effect, making sure the entire component tree gets updated, whatever happens. We have the ability to control this one way or another. We can explicitly detach and reattach the view from the change detection tree using [detach()](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/view%5Fref.ts?ref=angularspace.com#L220-L222) and [reattach()](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/view%5Fref.ts?ref=angularspace.com#L280-L283) methods respectively. ```typescript @Component({ selector: 'detached', template: `Detached Component` }) export class DetachedComponent { constructor(private cdr: ChangeDetectorRef) { cdr.detach(); } } ``` If we detach the view from the change detection, Angular will skip it, regardles of any changes chappend. --- ## OnPush Change Detection Strategy For better performance, Angular offers the `OnPush` change detection strategy. This strategy tells Angular to skip change detection for the component unless one of its inputs changes, we mark component to check using `markForCheck` method or an event occurs, for example a XHR request handled by an `async` pipe. By focusing on what's changed, rather than checking everything all the time, `OnPush` makes apps faster and more efficient, reduces unnecessary updates, improves performance, and prevents potential disasters. Using `OnPush` helps optimize performance by reducing the number of times change detection runs, especially in large and complex applications. So, how does it work? I mentioned that `OnPush` change detection gets triggered when we do things like setting a new Input value in a child component. Now, let's get back to the Angular source code, and verify the part that runs when we [set a new Input value](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/component%5Fref.ts?ref=angularspace.com#L442-L469). So, when the `setInput` method is called, it confirms that the Input value actually has been changed, ```typescript if ( this.previousInputValues.has(name) && Object.is(this.previousInputValues.get(name), value) ) { return; } ``` marking the component view as `Dirty` in the [markViewDirty](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/component%5Fref.ts?ref=angularspace.com#L460) method, which basically tells Angular, > "Hey, this child component's view needs updating, so take a look next time you check for changes." The same idea applies when, for example, we are using an [Async pipe](https://github.com/angular/angular/blob/18.1.0/packages/common/src/pipes/async%5Fpipe.ts?ref=angularspace.com#L102). It subscribes to observables, which updates the latest value by [calling the markForCheck function](https://github.com/angular/angular/blob/18.1.0/packages/common/src/pipes/async%5Fpipe.ts?ref=angularspace.com#L194), ```typescript private _updateLatestValue(async: any, value: Object): void { if (async === this._obj) { this._latestValue = value; if (this.markForCheckOnValueUpdate) { this._ref?.markForCheck(); } } } ``` which, in turn, triggers the [markViewDirty](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/view%5Fref.ts?ref=angularspace.com#L164) method. `markForCheck` is also used in a few other scenarios. For example if we want to initiate the change detection manually, we handle the DOM event, or attaching, detaching a view. And in all these scenarios at some point we call the [markViewDirty](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/mark%5Fview%5Fdirty.ts?ref=angularspace.com#L41-L50) method. ```typescript while (lView) { lView[FLAGS] |= dirtyBitsToUse; const parent = getLViewParent(lView); if (isRootView(lView) && !parent) { return lView; } lView = parent!; } ``` This method is like a domino effect – it starts with the specified view and moves up the family tree, by [checking if there’s a parent](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/mark%5Fview%5Fdirty.ts?ref=angularspace.com#L45), ```typescript if (isRootView(lView) && !parent) ``` marking each view along the path as [Dirty](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/mark%5Fview%5Fdirty.ts?ref=angularspace.com#L42), meaning they all need checking. ```typescript const dirtyBitsToUse = isRefreshingViews() ? LViewFlags.Dirty : LViewFlags.RefreshView | LViewFlags.Dirty; while (lView) { lView[FLAGS] |= dirtyBitsToUse; … } ``` And when it's time to do the actual checking, Angular [looks at those views marked as dirty](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/change%5Fdetection.ts?ref=angularspace.com#L475-L479). ```typescript shouldRefreshView ||= !!( flags & LViewFlags.Dirty && mode === ChangeDetectionMode.Global && !isInCheckNoChangesPass ); ``` If a component's view is marked as `Dirty`, it means something in that component has changed, and it needs to be re-rendered. And we do that by calling the [refreshView](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/change%5Fdetection.ts?ref=angularspace.com#L497) method. ![Click on Component D triggers an animation on the clicked element and all its ancestors, starting from the root component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zh36pb3hlvr8hobv25pq.gif) When we click on the component, it triggers a change detection in the component itself and all its parent components, which is exactly what we should expect, because we marked all its ancestors as `Dirty` in the `markViewDirty` method. --- ## Signals Era Since version 16, Angular has been introducing Signals - a wrapper around a value that can notify interested consumers when that value changes. It can contain any value, from simple primitives to complex data structures, and we can read the value through a getter function, which allows Angular to track where the signal is used. Now, the cool part is, Angular allows us to utilize this Signal power and combine it with the `OnPush` strategy to mark certain components for updates. This little trick eliminates the need for unnecessary checks on components, whether they're parent or child elements. Let's see what makes the signals work the way they do. Whenever we tweak a signal within a component's template, using methods like `set()` or `update()`, it's like setting off a little chain reaction. First, Angular calls [signalSetFn](https://github.com/angular/angular/blob/18.1.0/packages/core/primitives/signals/src/signal.ts?ref=angularspace.com#L71), which will [send out a notification](https://github.com/angular/angular/blob/18.1.0/packages/core/primitives/signals/src/signal.ts?ref=angularspace.com#L108) to the live consumer waiting in the view, ```typescript function signalValueChanged(node: SignalNode): void { … producerNotifyConsumers(node); … } ``` making it go, ["Hey, I'm dirty!"](https://github.com/angular/angular/blob/18.1.0/packages/core/primitives/signals/src/graph.ts?ref=angularspace.com#L304C9-L304C26) ```typescript for (const consumer of node.liveConsumerNode) { if (!consumer.dirty) { consumerMarkDirty(consumer); } } ``` Then, it [marks all its ancestors, right up to the root, with a flag called HasChildViewsToRefresh](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/util/view%5Futils.ts?ref=angularspace.com#L262), indicating > "Hey, I've got some child views here that need to be refreshed." ```typescript while (parent !== null) { if (parent[FLAGS] & LViewFlags.HasChildViewsToRefresh) { break; } parent[FLAGS] |= LViewFlags.HasChildViewsToRefresh; if (!viewAttachedToChangeDetector(parent)) { break; } parent = getLViewParent(parent); } ``` If we take a look closer, we'll see that this method is pretty similar to the `markViewDirty` method. The difference is, instead of marking all parent views with the `Dirty` flag, we mark them with the `HasChildViewToRefresh` flag. Now, as you already know, the change detection mechanism consists of two parts and the subsequent part traverses the tree of views. So let's back into our `detectChangesInView` method that evaluates whether a view requires updating. Now, here's the clever part - when a view is [marked with this HasChildViewsToRefresh flag](https://github.com/angular/angular/blob/18.1.0/packages/core/src/render3/instructions/change%5Fdetection.ts?ref=angularspace.com#L498-L504), there's no need to re-render it. Angular skips right ahead to checking out the child component view if there's any. ```typescript else if (flags & LViewFlags.HasChildViewsToRefresh) { detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Targeted); const components = tView.components; if (components !== null) { detectChangesInChildComponents(lView, components, ChangeDetectionMode.Targeted); } } ``` This smart shortcut helps Angular avoid wasting time on unnecessary stuff, ensuring smooth and efficient operation! When we use e.g. the `setInterval` function to update the signal value in `Component D`, only the components directly touched by the signal change actually get refreshed. ![Click on Component D, which triggers an animation only on itself](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awent00qlx3wc54z0vpn.gif) `OnPush` within Signals is like a sniper shot for detecting changes. It allows us to specify which components should be re-rendered when certain signals change. This means that instead of refreshing the entire component tree or a single branch when using `OnPush`, we only re-render the components directly affected by the signal change. --- ## Wrap up Default change detection relies on the `CheckAlways` flag, triggering updates for the entire view tree, whatever happens. This keeps everything consistent but can be overkill, causing unnecessary performance hits as the app grows. `OnPush` change detection is a powerful optimization technique in Angular. It ensures that components are only re-rendered when their inputs change, when events occur within the component or when we manually trigger change detection using the `markForCheck` method. It's important to note that when we use `OnPush`, the change detection refreshes not only the component itself but also all its parent components in the component tree. `OnPush` within Signals is a more targeted approach to change detection. It allows us to specify which components should be re-rendered when certain signals change. This means that instead of refreshing the entire component tree or a single branch when using `OnPush`, we only re-render the components directly affected by the signal change. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2024-11-13-at-15.52.00--1---1-.jpg) ### Reactive programming in Angular 101 URL: https://www.angularspace.com/reactive-programming-in-angular-101/ Last updated: 2025-01-08T08:35:27.000Z If you have been using Angular for a while, you may have heard terms like "reactive primitive", or "RxJS interoperability", or "declarative programming" a lot recently. I trust that you, at least on some level, understand what all of those mean; however, when it comes to real life, it is sometimes quite challenging to understand if a given piece of code is "reactive", "declarative", or whatever. It is even harder to change existing code to be reactive or declarative. With this series of articles, I will try to explain all of these concepts not only theoretically, but with actual examples, real-life scenarios, and some tips that will help you recognize patterns associated with reactivity, start writing code with a declarative approach in mind from the very beginning, and understand the limitation of these approaches. Also, we will try to make reactive programming less scary for developers who find themselves intimidated by bombastic-sounding concepts like "derived state", "side effect", and so on. For this purpose, this article will have callout notes to fix when we have learned a new term to keep it in mind and apply later. > Note: In these articles, I will touch different topics and terms from reactive programming, functional programming, and related fields; these articles are not comprehensive resources on those subjects and are limited to Angular applications; however, whenever appropriate, we will use some concepts from the aforementioned topics to better illustrate our approach. Do not worry - we will explain them all! ## So, what is reactive programming? I am tempted to say "reactive programming is when we react to something", and, honestly, that would not have been entirely wrong. However, there is more to it than that. I could say "reactive programming is when we react to events", and that would not have been entirely wrong either. But imagine someone who understands reactive programming looking at this code: ```ts document.body.addEventListener('click', e => { console.log('clicked'); }); ``` Will they sincerely call this code "reactive"? After all, it is reacting to an event, and we just gave two definitions with both of which this code is perfectly compatible. So what's the catch? Maybe, it will be easier to talk about what reactive programming is *not*, and from that we can at least have a tool that will help us deduce if our code is reactive. In further sections, you will notice that often explaining some terms related to reactive programming involves figuring out what the term does *not* mean, so this pattern will repeat itself a lot in these articles. With this in mind, let's talk about data. ### What is "reactive state"? Often, when we refer to data in frontend applications (but not exclusively), we call it "state". Why is that? Well, because the data (which is constantly changing during the lifetime of an application), has different values at different points in time, and we can only interact with a "snapshot" of that data at a given point in time. That snapshot is what we refer to as "state". In frontend applications, we usually have some initial state, a UI that is rendered based on that state, and events that trigger changes to that state, which in turn results in a new UI. But before we dive deep into that, let's first figure out the concept of "state" in general programming, not just frontend or any framework in particular. Let's discuss this very simple piece of code: ```ts let firstName = 'John'; let lastName = 'Doe'; let fullName = firstName + ' ' + lastName; console.log(fullName); ``` So, is this code reactive? Not really. While at first (on line 3), `fullName` does contain the actual `fullName` value, we can easily throw it off by adding a simple line of code: ```ts let firstName = 'John'; let lastName = 'Doe'; let fullName = firstName + ' ' + lastName; console.log(fullName); // logs 'John Doe' firstName = 'Jane'; console.log(fullName); // still logs 'John Doe' ``` Well, obviously the `fullName` variable *did *not* react* to the change in `firstName`. Of course, this is expected, these are just variables - small pieces of data that are constantly changing. Now, while this example is probably the simplest ever, what it did is actually define our problem (or at least a part of it) that we aim to solve with reactive programming, and that is keeping data in sync. Now, let's look at this example: ```ts class Person { constructor(public firstName, public lastName) {} get fullName() { return this.firstName + ' ' + this.lastName; } } const person = new Person('John', 'Doe'); console.log(person.fullName); // logs 'John Doe' person.firstName = 'Jane'; console.log(person.fullName); // now it logs 'Jane Doe' ``` Here, we did a clever trick; instead of storing the `fullName` in a variable, we realized that we don't actually need to "store" it, we just need to calculate it whenever we need it. This is only possible because `fullName` itself is not actually any data; it is just a representation of the actual data (`firstName` \+ `lastName`). In this case, the `fullName` "property" is fully dependant (note the word "fully" here, in the future, we will encounter cases where some state is only partially dependent on other state) on the `firstName` and `lastName` properties, so we do not need to store it, we can just compute it. Such computed states are often called "derived" states. Remember this term if it is unfamiliar to you. > New term: *Derived state* \- data that is dependant on other data, and that is updated every time one of its dependencies changes. So, is this reactive code? Not quite yet; the issue is, we learned we can derive new state based on other state, but what if our "reaction" to some state is not to simply compute, but perform a side effect? Wait wait wait, *side effect*. What is this? Yet another "scary" term. Let's figure it out. ### What is a side effect? To understand this, we need to talk about functional programming a tiny bit, mainly about the concept of *pure functions*. Let's talk about a particular function. This one: ```ts let count = 0; function incrementInFiveMinutes() { setTimeout(() => count++, 5_000 * 60); } ``` Now, this `incrementInFiveMinutes` function is *not* a pure function. Let's understand why. To do this, let's add some code: ```ts incrementInFiveMinutes(); document.querySelector('button').addEventListener('click', () => { console.log(count); }); ``` Now, let us ask ourselves a question. What number will be logged in the console? If you answered "it depends!", then surely you are getting to the point. Of course, if we run this program at 17:51, and click on the button at 17:53, it will log `0`, but if we click again at, say, 18:07, it will log `1`. However, our function ran once at 17:51 and since then, long stopped being any part of the program, however it still impacted the way our application functions. Now, let us consider the following code: ```ts let first = 7; let second = 3; function sum(a, b) { return a + b; } console.log(sum(first, second)); ``` Now, let us ask the same question - what will be logged in the console? Well, obviously, it will be `10`, because `first` and `second` are both `7` and `3` respectively, and `sum` just returns the sum of those two numbers. Now, it was very easy for us to see this, because `sum` is, in fact, a pure function. It only takes any data in the form of arguments, does not modify them in any way, and does not touch any other data in the outside world. It is easy to see why pure functions are so great; as we just saw, they are very predictable, we do not need to run them to see what they do, and when explaining them, we never have to say "well, it depends". > New term: *Pure function* \- a function that does not modify any data in the outside world, and that only takes data as arguments. Now, let us go back to the `incrementInFiveMinutes` function and see what it is that made it "impure". Well, we scheduled a timeout, which is a mechanism that is outside of this function - obviously `setTimeout` is defined elsewhere and is a global function, and, furthermore, we also changed the value of `count` in the callback, which is also a global variable. So, we indirectly impacted the application state or some external system. This is what a side effect is! > New term: *Side effect* \- a change in the state of the application or some external system that is not directly related to the function's arguments or return value. With this in mind, we can simplify the "pure function" definition. > Updated term: *Pure function* \- a function that does not have any side effects It is important to understand that functional purity is a **very** strict concept. It is very easy to break a function's purity, simply by referring to something out of its scope. Furthermore, while pure functions are predictable, beautiful, safe and so on, it is impossible to build a useful frontend application with only pure functions. Take a look at this simple Angular code: ```ts @Injectable({providedIn: 'root'}) export class ProductService { readonly #http = inject(HttpClient); addProduct(product: Product) { return this.#http.post('my.api.com/products', product); } } ``` Now, the `addProduct` method definitely has a **big** side-effect: it modifies a database that is probably located on another continent. However, it is also a very important function, as it allows us to build applications that let users add, delete, update and otherwise manage products. So, as we can see, "pure functions" is not about having only pure functions, but rather, about separating "pure logic" from "data modification" or other processes that we have to do just because we work with computers. Now, having learned about pure functions and side-effects, we can finally add the missing puzzle of "reactive state". If we bring ourselves mentally back to the end of the previous subsection, we will remember that we defined "derived state" and mentioned that state being "derivable" is not enough to call it reactive, and we need to be able to also perform side-effects whenever that state changes. We brought forth the example of writing a reactive value in a way that its value will be stored in `localStorage`, which, as a keen reader might notice, is also a side effect. So, how about we try to create a reactive value wrapper such that it will be possible to derive new reactive values from it, and also to perform side-effects whenever that value changes, all by ourselves? Turns out, we can make a primitive version of this pretty easily. ```typescript export class ReactiveValue { #value: T; #sideEffects: ((value: T) => void)[] = []; constructor(value: T) { this.#value = value; } getValue() { return this.#value; } setValue(value: T) { if (this.#value !== value) { this.#value = value; this.#sideEffects.forEach(sideEffect => sideEffect(value)); } } onChange(sideEffect: (value: T) => void) { this.#sideEffects.push(sideEffect); } } ``` Now, with this code, we can create a reactive variable and do whatever we want in side effects: ```typescript const count = new ReactiveValue(0); count.onChange(value => localStorage.setItem('count', value)); count.setValue(7) ``` > Warning: this code is simply a demonstration, and is heavily simplified; do not use it in production-ready applications; if you are using Angular, utilize reactive primitives provided bt the framework instead (signals, linked signals, and so on). Now, what can we see here? Well, we can create a reactive state, update it, and be sure that its updates will trigger relevant side effects. This is a big step towards understanding reactive programming, but it is not final - we have yet one final concept to learn before we can say that we grasp the fundamentals of reactive programming, and that is declarative code. ### Declarative Code To understand declarative code, we first need to take a look at spaghetti. ![spaghetti.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/spaghetti.png) No, not that spaghetti! This spaghetti: ```typescript @Component({...}) export class ProductComponent implements OnInit, OnChanges { @Input() productId: number; readonly #productService = inject(ProductService); readonly #userService = inject(UserService); product: Product; currentUser: User; ngOnInit() { this.#userService.getCurrentUser().subscribe( user => this.currentUser = user, ); } ngOnChanges() { this.#productService.getProduct(this.productId).subscribe( product => this.product = product, ); } } ``` While this is a small example and we might even wonder "why is this code so bad to call it "spaghetti"?", we can think about it in terms of reactivity (so far as we understood it), and figure it out for ourselves. 1. The component has an input of `productId` 2. `product` depends on `productId`, as we cannot load a product without its id 3. We can then say that `product` is reacting to `productId` 4. We also have a `currentUser` property which is being loaded separately. Now, this is the sort of code that one might call "imperative", which is the opposite of "declarative" in a broad sense and is a (relatively) "bad" thing for the lack of a better words. Let's deconstruct what "imperative code" means using a simpler example: ```typescript const users = [ { name: 'John', age: 30 }, { name: 'Jane', age: 25 }, { name: 'Bob', age: 35 }, { name: 'Alice', age: 16 }, ]; let result = []; for (const user of users) { if (user.age > 18) { result.push(user); } } ``` Now, this is also some code that I would call "imperative". So why is that? Well, to understand it, let's pretend we did not write this code, and are instead a developer who is reading this piece of code. Here are the mental steps we would probably take: 1. Okay, there is an an array of user objects 2. Hmmm, there is a new, empty array called `result` 3. *Mental detour*: probably some logic is going to be performed on the `users` array, and the result is going to be accumulated in the `result` array 4. Okay, there's a `for` loop, let's see what it does 5. Okay, it checks if the user is older than 18... I see where this is going... 6. Yeah, sure, it pushes it in the `result` array! Okay, got it, it filters adult users Now, with this code, we went through 6 mental steps with one *detour* to understand what it does. This is because this code describes *how* to filter adult users. Now, let use see the same code, but better: ```typescript const users = [ { name: 'John', age: 30 }, { name: 'Jane', age: 25 }, { name: 'Bob', age: 35 }, { name: 'Alice', age: 16 }, ]; let result = users.filter(user => user.age > 18); ``` Now, not only is this code shorter, it is also simpler to both explain and digest. Let's do the same mental exercise: 1. Okay, there is an an array of user objects 2. Ah, we are filtering this array... let's see on what condition 3. The condition is `age > 18`... ah, okay, we filtered the adult users! As we can see, grasping this piece of code is roughly 2 times easier in terms of mental steps we take than the previous one. Also notice that we purposefully named the new array `result` and not `adults` or `adultUsers`, as we would do in real life, to further illustrate that the second approach is superior to the first one even when the namings aren't quite great. Okay, so what is it then which makes the second approach better? Actually it is fairly simple to explain. The first example is describing, step-by-step, *how* to filter the adults from a list of users; we need to first read the implementation to deduce *what* it does. The second example, on the other hand, is explicitly telling us *what* it is doing; the process of *how* it is exactly filtering the adults from the `users` array is hidden (and, frankly, unimportant), which is why we almost immediately understand what it tries to achieve. And this is, for the most part, the definition of declarative and imperative code styles. > New term: *imperative code*: a piece of code which describes, in distinct programming steps, *how* to achieve a result which the program wants to achieve > New term: *declarative code*: a piece of code which describes, usually in one or few steps, *what* the program wants to achieve Now, let us notice several things about these concepts: 1. Declarative programming almost always involves some level of abstraction: for instance, the `filter` method abstracts away the process of setting up a new array, looping thorough the existing one, performing the checking and then pushing the results in this new array. We only provide the name `filter` and the condition to check, and the method does the rest. 2. Imperative code can be transformed in a way that it becomes (kind of) declarative. The best example of this is creating functions (especially pure functions) that contain the imperative code, and then using those functions. For instance, in the first example we might have kept the code the same and just put it into a function named `filterAdults`, and it would have been more declarative than what we had initially 3. Both terms are not super rigid and involve a "spectrum"; this is why we used the word "more declarative" earlier 4. Despite those terms not having precise scientific accuracy, they are very useful in helping us evaluate the quality of our code Now, we learned two definitions that will help us understand why the previous Angular component is poorly written (you guessed it - it is imperative code). Let's review the same example, now with comments explaining step-by-step where the problems are: ```typescript @Component({...}) export class ProductComponent implements OnInit, OnChanges { @Input() productId: number; // no problems here, components sometimes have to have input properties readonly #productService = inject(ProductService); // just DI readonly #userService = inject(UserService); // just DI product: Product; // okay, we have a definition of a property called `product`, but it's just s definition; there is no way to understand what it contains, how it value is updated, and so on currentUser: User; // same here, `currentUser` seems to be produced from thin air ngOnInit() { // using `ngOnInit` itself is not reactive in terms that it doesn't really explain what is going to be done this.#userService.getCurrentUser().subscribe( // imperative code - we directly subscribe to the observable user => this.currentUser = user, // imperative code - we explain *how* the `currentUser` property receives its value ); } ngOnChanges() { // same this.#productService.getProduct(this.productId).subscribe( // same product => this.product = product, // same ); } } ``` With this, so far, we learned about reactive values, derived state, pure functions, side effects, and declarative code. This is a great base to finally define what reactive programming is, and to do that, we will again use examples to reconcile these concepts into one coherent understanding of the topic. ### Defining reactive programming So far, all we did was talk about *state* \- data that changes over time. However, and this is important, *not everything is a state*. Let's see the following simple example to understand what we mean by that: ```typescript document.body.addEventListener('click', () => { console.log('clicked'); }); ``` In this case, we do not have *any* state, but we do, however, have a side effect. So, what triggers the side effect, if not a state change? Well, obviously, an event. Now, events are a bit abstract in the sense that it is hard to very clearly define what is and what isn't an event. Clearly, a user clicking a button is an event - an arising asynchronous thing that may trigger action down the line. But, some may argue that a change to reactive state, which we described above, is also an event, which, on the surface, makes sense. However, if we are going to do proper reactive programming, we need to be able to distinguish between events and reactive state changes. This is because of two important distinctions, which we will discuss right now. #### Events and state change - what's the difference? First of all, an important distinction is that state always has a "current" value. In the example above, we created a reactive variable, and it had a value, and when we changed it, not only the side effects were triggered, but the value of the reactive variable changed as well. And we could, always, without having to rely on side effects, read the current value of the reactive variable. It is way different when it comes to events. The very previous example we used, with the click event, does not have a concept of "current" value. If we think about it, well, what is the "current" click? We could introduce such a concept by storing the last click event, but that is only a small subset of what we could do there, because, well, how is it better and more useful than the "first" click event? So, in that sense, events, compared to state changes, are [ephemeral](https://www.merriam-webster.com/dictionary/ephemeral?ref=angularspace.com) (for the lack of a better word) - they exist momentarily and pass away as soon as we are done reacting to them. If we continue this line of thinking, we can kind of say that the `addEventListener` handler is a "pure side effect" (as much as it sounds self-contradictory!), because it handles an event, but is not associated with any state. Next, there is a more technical distinction between events and state changes. Events are by design asynchronous - we cannot predict when they can happen, even if at all (user might not click anywhere forever, for instance). On the other hand, state changes are synchronous - any time we update the state, the associated side effects are triggered, and derived states can be updated immediately too (this is not always the case in real life, as we will see in the next article). It is important to understand that both of these distinctions, while more or less strict, are not *always* like that. Sometimes, some reactive concepts exist on the edge between the two. Take a look at this example: ```typescript let todos = []; fetch('https://jsonplaceholder.typicode.com/todos') .then(res => res.json()) .then(result => { todos = result; }); ``` Now, the second callback of the `then` method is clearly a side effect, data coming from an external system (backend API) modifying a local variable, but it also is associated with a reactive value - the todos variable, which we could simply modify further down the line. This scenario is important for our further explorations in the next article. Finally, we are ready for a sufficient definitions of reactive programming! > Reactive programming is the practice of writing software wherein application data (the state) is reactive, meaning we can perform side effects on their state changed and derive new state from previous reactive values, and where we react to both asynchronous events and synchronous state changes in a declarative manner. This is kind of a mouthful, which is why I prefer a shorter (joke) definition: reactive programming is when we react to things declaratively. Finally, before wrapping up this first article, let's talk about reactive tools that Angular provides to solve these sorts of problems - after all, the article is titled "Reactive Programming in Angular", and we have not talked about Angular too much yet. ## Reactive Tools in Angular Now, here, we leave the realm of abstract concepts in favor of quite strict definitions. Angular has exactly 2 toolsets for reactive programming: RxJS and Signals. Let's briefly discuss both of them. ### RxJS RxJS should be strictly used for handling events and not reactive state changes. Note that we say "should", as it is very possible to use RxJS for reactive state changes, but it is not the best practice - RxJS is more tailored for handling streams of events, rather than state. It is quite a rich toolkit (async programming usually is harder, we deal with timing, cancellation, async errors, hence more complexity). However, the big thing about RxJS is that it allows handling events in a declarative fashion. Remember the `addEventListener` example? Well, it was obviously quire imperative, (we told how to handle an event), and not very extendable (we couldn't really modify the event listener function after defining it). Instead, with RxJS, we can declare a stream of event clicks, and use it in multiple ways however we see fit: ```typescript const clicks$ = fromEvent(document, 'click'); clicks$.subscribe((event) => { console.log('Clicked'); }); ``` Here, instead of *imperatively* telling the program *how* to handle the event, we *declared* a stream of events, and then described *what* we are doing to it. We can also derive new streams from this existing one, as our definition of reactive programming tells us: ```typescript const clicks$ = fromEvent(document, 'click'); const ctrlClicks$ = clicks$.pipe(filter(event => event.ctrlKey)); ctrlClicks$.subscribe((event) => { console.log('Ctrl-clicked'); }); ``` Here, we have a stream of clicks, and we derived a new stream of clicks that only include clicks with the `ctrlKey` pressed. The original stream is not modified in any way, and if we wish, we can subscribe to it separately and do something with *all* clicks, or, instead, we can work only with those clicks that have the `Ctrl` key pressed. So, RxJS is a remarkable tool for working with asynchronous events in a declarative manner - fantastic reactive library! We will talk *a lot* about it in subsequent articles. ### Signals Signals are a relatively small toolkit (at least when compared to RxJS), which allows us to define a reactive state, update it, derive new reactive states from it, and perform side effects upon its state changes. As we can see, Signals, on their part, are reserved strictly for reactive state, and not asynchronous events. If you worked with the latest versions of Angular, you will be at least somewhat familiar with them, but here is a small example: ```typescript @Component({ template: ` {{ count() }} {{ doubleCount() }} ` }) export class MyComponent { count = signal(0); doubleCount = computed(() => this.count() * 2); constructor() { // save the latest count to local storage effect(() => { localStorage.setItem('count', this.count()); }); } increment() { this.count.update(n => n + 1); } decrement() { this.count.update(n => n - 1); } reset() { this.count.set(0); } } ``` As we can see, all the necessary conditions for reactive programming are there, the value is created declaratively, we have derived state with `computed`, and we have side effects with `effect`. ### Interoperability between the two As we mentioned, handling both reactive state changes and events is crucial for having reactive code. Furthermore, as we also saw, some of the reactive values exist on the edge between events and state, like HTTP calls. So, keeping this in mind, it is important for great developer experience to be able to seamlessly switch between state and events, or, in case with Angular, from signals to RxJS and vice versa. Of course, this is possible with `toSignal` and `toObservable` functions and some other utilities, which we will explore in full detail in the next article. ## Conclusion In this article, we learned the theoretical basis (while looking at examples) of reactive programming, understood what state and derived state are, what are pure functions and side effects and why it is important to recognize them, learned about events and state changes and how they are related, and finally, introduced RxJS and Signals, the reactive toolkits available for Angular. In the next article, we will start building software by actually using those tools, enhancing our newly acquired knowledge with some work in the fields. ## Small promotion ![Modern Angular.jpeg](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Modern-Angular.jpeg) In this article we discussed signals and RxJS interoperability. The recent upgrades in reactive programming in Angular have caused many developers to be confused about what solutions to chose, how to implement them, and how to migrate their existing codebases to the most recent features. Thankfully, I have a response to this concern: very soon, my very first book is going into print! It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular?ref=angularspace.com). The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on [Twitter](https://twitter.com/Armandotrue?ref=angularspace.com) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/?ref=angularspace.com), where I will be posting whenever there are news or promotions available. P.S. Hey! Check out chapter 5 of my book to learn about RxJS interoperability and chapters 6-7 for a deep dive into signals ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/01/Screenshot-2024-11-13-at-15.52.00--1-.jpg) ### 5 CSS snippets every front-end developer should know in 2024 URL: https://www.angularspace.com/5-css-snippets-every-front-end-developer-should-know-in-2024/ Last updated: 2024-12-31T08:33:16.000Z Toolbelt worthy, powerful, and stable CSS you can use today. I believe every front-end developer should know `:has()` is more than a "parent selector", the how and why of a `subgrid`, how to nest with built-in CSS syntax, how to let the browser balance headline text wrapping, and how use container query units. This post is a continuation of [last year's 6 CSS snippets every front-end developer should know in 2023](https://web.dev/articles/6-css-snippets-every-front-end-developer-should-know-in-2023?ref=angularspace.com). ## CSS:has(.potential-beyond-being-a-parent-selector) --- `:has()` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-31-at-09.25.26.png) Source: [https://developer.mozilla.org/docs/Web/CSS/:has](https://developer.mozilla.org/docs/Web/CSS/:has?ref=angularspace.com) `:has()` landed across all major browsers at the end 2023! This new selector seems small and innocent but you'll be surprised at all the use cases it can unlock: games, reactivity, content aware layout, smart components, and [much more that's well explored in this article by Jhey](https://developer.chrome.com/blog/has-m105?ref=angularspace.com). ![4 panels shown, each with an image and caption. Each image shows a brain activating more and more brain power. The first panel is says :has(). Second panel says figure:has(caption) as a parent selector. Third panel says .layout:has(> :nth-child(5)) as a quantity selector. Forth panel says html:has(#checked) .new-subject as conditional subject changing selector.](https://web.dev/static/articles/5-css-snippets-every-front-end-developer-should-know-in-2024/image/has-meme.jpg) Here's a couple examples of using `:has()` as a parent selector. It got this name because usually the subject of a selector is at the end, like `ul li`, where `li` is the subject and gets the styles. With `:has()`, the element at the beginning of the selector can become the subject. In the following example, the button has a gap if there's an element inside with a class of `.icon`. The card changes orientation if there's an image inside. ```CSS button:has(.icon) { gap: 1ch; } .card:has(img) { grid-auto-flow: row; } ``` A long desired selector is to change a layout based on how many items it has. This is now possible with `:has()` because it can keep the container as the subject while querying the number of children. ```CSS main:has(> :nth-child(5)) {…} ``` Another higher level example, change styles set on the entire document when a specific checkbox on the page is enabled: ```CSS html:has(#dark-mode:checked) {…} ``` These are simple use cases that don't change the subject of the selector, if you just look at examples like this, you might think `:has()` is limited to being a parent selector. Consider the following examples though. These check for something based on a common ancestor, then pivot the selector subject to a child somewhere deeper in the page. This one shows a form error element if any of its inputs are invalid: ```CSS form:has(:user-invalid) .error { display: block; } ``` This one slides out the main content area when a sidenav has a class of `.--is-open`: ```CSS html:has(#sidenav.--is-open) main { translate: -320px; } ``` Here's a fun demo that uses `:has()` as a parent selector, `:has()` with quantity queries, and container queries to make a grid layout that's capable of elegantly displaying 1-12 elements in portrait or landscape orientations: 0:00 /0:48 1× ## Create a subgrid --- `subgrid` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-31-at-09.25.40.png) Source: [https://developer.mozilla.org/docs/Web/CSS/CSS\_grid\_layout/Subgrid](https://developer.mozilla.org/docs/Web/CSS/CSS%5Fgrid%5Flayout/Subgrid?ref=angularspace.com) For many years the front-end web community asked for subgrid to help round out and finish the massively popular and powerful CSS grid layout engine. It's now available in all three major engines. [Learn more about subgrid here](https://web.dev/articles/css-subgrid?ref=angularspace.com), if you'd like an overview. ```CSS body { display: grid; grid-template-columns: repeat(auto-fill, minmax(30ch, 1fr)); > article { display: grid; grid-row: span 4; grid-template-rows: subgrid; } } ``` See the Pen [subgrid content alignment](https://codepen.io/web-dot-dev/pen/abMmPqj?ref=angularspace.com) by web.dev ([@web-dot-dev](https://codepen.io/web-dot-dev?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). ## Nest the CSS way --- `nesting` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-31-at-09.26.03.png) Source: [https://developer.mozilla.org/docs/Web/CSS/Nesting\_selector](https://developer.mozilla.org/docs/Web/CSS/Nesting%5Fselector?ref=angularspace.com) Built-in CSS nesting became available in all major browsers in 2023\. I even updated my website to remove the build process that compiled nesting away, and now I ship a smaller stylesheet! Yep, stylesheets with nesting are smaller and all the browser devtools are ready to represent it. You can find an overview of the [CSS nesting syntax here](https://developer.chrome.com/docs/css-ui/css-nesting?ref=angularspace.com), for all the details. The following code example shows a syntax example. ```CSS .you { .can-totally-ship-this { &.if-you-wanted { /* it's VERY MUCH like SCSS */ &:is(:hover, :focus-visible) { /* put a bird on it */ } } } } .for-theming { @media (prefers-color-scheme: dark) { /* you can nest media queries */ } } /* or for theming with [data-theme="dark"] */ .button { background: black; color: white; /* nest and do more than just append, flip it and reverse it */ [data-theme="dark"] & { background: white; color: black; } } ``` ## Let the browser balance headlines --- `balance` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-31-at-09.26.13.png) Source: [https://developer.mozilla.org/docs/Web/CSS/text-wrap#balance](https://developer.mozilla.org/docs/Web/CSS/text-wrap?ref=angularspace.com#balance) `pretty` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-31-at-09.26.21.png) Source: [https://developer.mozilla.org/docs/Web/CSS/text-wrap#pretty](https://developer.mozilla.org/docs/Web/CSS/text-wrap?ref=angularspace.com#pretty) Without `text-wrap: balance`, developers and copy writers are left to line break hints such as`` elements or `­`. It's mostly a losing battle because as soon as the content is translated, zoomed or modified in any way, there's no guarantee that those wrapping hints will be in the right place for the new presentation of the content. With balanced text wrapping, you can leave this work to the browser. You can see a comparison in the following Codepen. See the Pen [subgrid content alignment](https://codepen.io/web-dot-dev/pen/abMmPqj?ref=angularspace.com) by web.dev ([@web-dot-dev](https://codepen.io/web-dot-dev?ref=angularspace.com)) on [CodePen](https://codepen.io/?ref=angularspace.com). ## Use container query units --- `cqw` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-31-at-09.26.39.png) Source: [https://developer.mozilla.org/docs/Web/CSS/container-type](https://developer.mozilla.org/docs/Web/CSS/container-type?ref=angularspace.com) Last year's post suggested that every front-end developer should know how to write a container query. If you've not yet learned, make 2024 the year to take the plunge, and check out container query units too. As an overview, [Ahmad Shadeed wrote a great article about container query units](https://ishadeed.com/article/container-query-units/?ref=angularspace.com) in 2021. There are six new **c**ontainer **q**uery units: 1. An inline variant `cqi`. 2. A width variant `cqw`. 3. A block variant `cqb`. 4. A height variant `cqh`. 5. A variant for whichever length is smaller `cqmin`. 6. A variant for whichever length is larger `cqmax`. Consider a scenario for relative and intrinsic animations to a container. A child element that slides out entirely from its container by using 100cqi—that's 100% of the container inline size. ```CSS @keyframes slide-out-of-container { to { translate: -100cqi; } } ``` Here's a card with container responsive typography, and an image that adapts to the orientation of the container, becoming half the size if the orientation is landscape. ```CSS .card { :is(h2,h3) { font-size: clamp(1.5rem, 5cqi, 4rem); } img { inline-size: 100cqi; @container (orientation: landscape) { inline-size: 50cqi; } } } ``` If these units are new to you it's probably a good idea to [review all the units available to you in 2024](https://nerdy.dev/new-relative-units-ric-rex-rlh-and-rch?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-11-13-at-15.52.00--1-.jpg) ### 10x e-Book Giveaway! - Learning Angular 5th Edition by Aristeidis Bampakos URL: https://www.angularspace.com/10x-e-book-giveaway-learning-angular-5th-edition-by-aristeidis-bampakos/ Last updated: 2024-12-21T19:27:05.000Z Time for another Angular Space Giveaway. Angular X-mas Calendar Edition 😄 Giveaway with good old fashioned Discord Raffle! Aristeidis Bampakos - has released many many books over the years. Time for 5th Edition Early Copy with 8th Chapters + Access to final copy when it's out 😄 I have 10x to giveaway thanks to PACKT & Aristeidis Bampakos. More details about the book here: [https://www.packtpub.com/en-us/product/learning-angular-9781835087480?type=subscription&srsltid=AfmBOoohJpFE1XRDvazUTtQOai2HzzyM0rfak3aoM5f5X4pVWcnzwOqM](https://www.packtpub.com/en-us/product/learning-angular-9781835087480?type=subscription&srsltid=AfmBOoohJpFE1XRDvazUTtQOai2HzzyM0rfak3aoM5f5X4pVWcnzwOqM&ref=angularspace.com) Instructions on how to participate in giveaway below 👇 _This post is for subscribers only._ ### Mastering Component Communication in Angular URL: https://www.angularspace.com/mastering-component-communication-in-angular/ Last updated: 2024-12-16T09:15:55.000Z ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/thumbnail-1.png) ## Intro Hey Angular devs! This guide explores how components can communicate with each other in your applications - from simple one-way data binding to more complex interactions, like passing data via router. While there are many different methods for component communication in Angular, we'll focus on providing an overview of different approaches without diving too deep into each one, because that would make this post endless. I assure you that some techniques mentioned in this article will work better in certain situations, so it's essential to know all available options, proper use cases and differences. This helps you to choose the best solution for your application through your critical thinking and understanding the context of the problem. Every approach will be explained with [code examples](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app?ref=angularspace.com), that you can run and test by yourself. The code examples shown in this blog post will be as simple, as possible, so I really recommend you to check the full examples in the mentioned code repository. ### Here's what we'll cover - Input and Output - `@Input` and `@Output` decorators - Brand new `input()` and `output()` functions - Setter methods with `@Input` decorator - `Input` and `Output` inheritance - `OnChanges` lifecycle hook with `@Input` decorators and `computed()` alternatives - `@Injectable` Services - Component/Directive injection - Template variables (`#`) - Content Projection - `@ContentChild` and `@ContentChildren` with `` - `contentChild()` and `contentChildren()` with signals - View and Query List - `@ViewChild` and `@ViewChildren` decorators - `viewChild()` and `viewChildren()` with signals - Routing - Routing Parameters and Queries (`/:id` and `?query=param`) - Routing with `Input` and `withComponentInputBinding()` function - Routing State Objects ## `Input`, `Output`, `Setter` and `ngOnChanges` Lifecycle Hook ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/input-output.png) ### `Input` and `Output` in Angular Let's explore the fundamental way components talk to each other in Angular - our first and probably most famous pair: `Input` and `Output`. We'll look at both traditional (decorator with `@`) and modern (functions) approaches to handle this communication. But before that, let's explore some practical uses of `Input` and `Output` and when to use them. #### 💡 Examples of Practical Uses of `Input` and `Output` 1. Develop more interactive components that notify parent elements of changes, e.g. dropdown menu or search. 2. Pass data like user details to a child component and `Output` to emit user update events from the child to the parent component. 3. Navigate from a product list to a detailed view using `Input` to pass the selected product ID to the detail component. | Good/Bad | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------------- | | ❌ | Providing Input and Output via metadata properties can be harder to understand and can be less concise. | | ❌ | Component inheritance is rarely used in Angular, so you may never need this. | | ⚠️ | With OnPush, changing object properties won't update the view - you must assign a new object reference. | | ✅ | It's the standard way to communicate between components, well-tested and recommended. | | ✅ | The newest Angular version lets you transform data through @Input() decorator's metadata transform function, similar to Setter. | | ✅ | Always good to use and recommended with signals (with Signals from Angular 17+ as input() functions). | | ✅ | input() and output() provide improved performance and change detection. | | ✅ | Two-way binding simplifies code by reducing boilerplate for managing Input/Output pairs in common scenarios. | | ✅ | Usage of model() function offers unification of Input and Output, which is typically useful in two-way data binding approaches. | #### Traditional Approach with Decorators The classic way uses `@Input()` and `@Output()` decorators, which let child components receive data and send it back to their parents. To achieve this, we need to pass our data through brackets `[searchTerm]="myData"`, and if we need to receive data back, we need to assign it as `(searchTermChange)="handle($event)"` to handle the event. ```typescript // Component with traditional `Input` and `Output`. @Component({ selector: 'app-search-box' }) class SearchBoxComponent { // Receives data from parent. @Input() searchTerm = ''; // Sends data to parent. @Output() searchTermChange = new EventEmitter(); } // Using in parent template with two-way binding approach. ``` #### Modern Approach with `input()` and `output()` Angular 17+ introduces a powerful new way using signal `input()` signal function and `output()` function. This approach offers better performance, smarter change detection and smoother integration with signals architecture, making it the go-to choice for new applications. ```typescript // Modern approach with signal `input()` function and `output()` function. @Component() class SearchBoxComponent { initialValue = input(); searchTermChange = output(); } ``` #### `Input` and `Output` Inheritance While it's not a common practice in Angular projects, Angular supports inheriting `Input` and `Output` properties from parent components. ```typescript // Parent component with `Input`. @Component({ selector: 'app-base-card', }) class BaseCardComponent { @Input() title = 'Header'; } // Child component inheriting parent's `Input`. @Component({ selector: 'app-product-card', }) class ProductCardComponent extends BaseCardComponent { // Child gets access to `title` property. constructor() { super(); console.log(this.title); } } ``` ### `Setter` Methods ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/setter.png) Want more control over your `Input`? Angular's `Setter` methods let you intercept and handle `Input` values before they're set. Let's firstly evaluate it's pros and cons: | Good/Bad | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | ❌ | Requires additional property for storing the value. | | ❌ | More verbose than simple Input declarations. | | ❌ | Input setters are executed individually, potentially leading to race conditions, if setters depend on the state of other inputs. | | ❌ | Updating e.g. global updates from Setter or lifecycle hooks can cause NG0100 ExpressionChanged error. | | ⚠️ | Improper use can cause side effects that you may not want (sometimes you might want them). | | ✅ | Signals resolve these issues by ensuring a consistent state across all inputs and removing order dependencies entirely. | | ✅ | Enables input validation on the fly. | | ✅ | Allows data transformation as values come in. | | ✅ | Can trigger side effects when new values change. | ```typescript // Example of `Setter` usage. @Input() set name(value: string) { console.log('New name:', value); // Store the value in component from setter. this._name = value.trim(); } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/1-input-output?ref=angularspace.com). --- ### Understanding `ngOnChanges` Lifecycle Hook ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/ng-on-changes.png) Now, let's explore `ngOnChanges`, a helpful lifecycle hook in Angular that tracks changes to your component's input values. When `Input` change, Angular automatically runs this method, providing you with `SimpleChanges` argument that tell you three key things: what changed, if it's the first change, and both the old and new values. #### 💡 Examples of Practical Uses of `ngOnChanges` 1. Implementing undo/redo functionality by tracking previous values. 2. Validating dependent `Input` when multiple `Input` change together (like form validation rules). | Good/Bad | Description | | -------- | ------------------------------------------------------------------------------------------------------- | | ❌ | Executes on every input change, which may affect performance if not used carefully. | | ❌ | Runs for all input changes, even when you're interested in specific ones only. | | ❌ | Requires setting up additional properties to track changes. | | ❌ | It should be never used with signal Input. That's useless since we have computed signals. | | ⚠️ | Runs first before ngOnInit Lifecycle Hook. | | ⚠️ | Improper use can cause side effects that you may not want. | | ⚠️ | With OnPush, changing object properties won't update the view - you must assign a new object reference. | | ✅ | Efficiently handles multiple Input changes in a single lifecycle hook. | | ✅ | Provides easy detection of first-time changes to Input properties. | | ✅ | Enables comparison between previous and current Input values. | ```typescript // Component that tracks `Input` changes with `ngOnChanges`. @Component() class NameDisplay implements OnChanges { @Input() name = ''; @Input() title = ''; // Adding a second input for title (Mr., Ms., Dr., etc). greeting = signal('Hello!') ngOnChanges(changes: SimpleChanges) { // Combine both inputs whenever either changes. if ('name' in changes || 'title' in changes) { const currentName = 'name' in changes ? changes['name'].currentValue : this.name; const currentTitle = 'title' in changes ? changes['title'].currentValue : this.title; // Create combined greeting. const fullGreeting = currentTitle ? `Hello, ${currentTitle} ${currentName}!` : `Hello, ${currentName}!` if ('name' in changes) { console.log('Name changed:', changes['name'].previousValue, '->', currentName); } if ('title' in changes) { console.log('Title changed:', changes['title'].previousValue, '->', currentTitle); this.greeting.set(fullGreeting); } } } } ``` ```typescript // Alternative solution that tracks `Input` with computed signals instead. @Component() class NameDisplay { name = input(''); title = input(''); // Create a computed signal that automatically updates when inputs change. greeting = computed(() => { const currentName = this.name(); const currentTitle = this.title() console.log('Name or title updated:', { name: currentName, title: currentTitle }) return currentTitle ? `Hello, ${currentTitle} ${currentName}!` : `Hello, ${currentName}!`; }); } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/2-input-ng-on-changes?ref=angularspace.com). --- ## Services in Angular ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/service.png) Services are a fundamental feature in Angular that serve multiple purposes, with one of their most powerful uses being data sharing between components. When provided at the `root` level, services create a centralized way for components to communicate and exchange information effectively. Think of a service as a central hub where components can store and access shared data. Any component can read from or write to this hub, creating a smooth two-way flow of information. #### 💡 Examples of Practical Uses of Services 1. Store user login states, preferences, and session tokens, providing a consistent user experience across different parts of the application. 2. Manage all backend API calls from a single service, simplifying the process of fetching, posting, and handling data across components. 3. Creating utility functions used across multiple components (formatters, validators). | Good/Bad | Description | | -------- | ---------------------------------------------------------------------------- | | ❌ | Requires understanding of Angular's dependency injection system. | | ⚠️ | Simple class that can be injected, usually used with Signals or Observables. | | ✅ | Enables component communication without creating direct dependencies. | | ✅ | Works across multiple components throughout your application. | | ✅ | Provides a centralized place for sharing data and logic. | | ✅ | Makes testing easier by separating concerns. | ```typescript // Service that manages shared data. @Injectable({ providedIn: 'root' }) class CartStore { cart = signal(['candy', 'chips', 'soda']); addItem(item: string) { this.cart.set([...this.cart(), item]); } } // Component using the shared service. class CartComponent { cartStore = inject(CartStore); cart = this.cartStore.cart; addItem(item: string) { this.cartStore.addItem(item); } } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/3-service?ref=angularspace.com). --- ## Template Variables in Angular ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/template-variable.jpeg) Template variables are a really cool feature in Angular marked by the `#` symbol in the template. Think of them as quick references you can create in your template to connect parent and child components. It's like giving your components nicknames they can use to talk to each other! #### 💡 Examples of Practical Uses of Template Variables 1. Managing component state from parent templates (expand/collapse panels, pagination controls). 2. Form manipulation (accessing form values, triggering validation, resetting forms). 3. Quickly access and manipulate DOM elements directly from the template without additional logic in the component class. | Good/Bad | Description | | -------- | ------------------------------------------------------------------------------------------ | | ❌ | Limited scalability due to tight coupling between components. | | ❌ | Variables are only accessible within the template unless passed through events. | | ❌ | Timing issues can occur if accessing elements before they're rendered. | | ✅ | Enables bi-directional communication between parent and child components within templates. | | ✅ | Works smoothly with ViewChild and template functions for element access. | | ✅ | Provides quick, direct access to component references. | | ✅ | Reduces boilerplate code by eliminating need for Input, Output, or services. | | ✅ | Gives parent components full access to child methods and properties. | ```typescript // Child component with todo management. @Component() class TodoListComponent { todos = ['Learn Angular', 'Build an app']; addTodo() { this.todos.push(`New Todo ${this.todos.length + 1}`); } } // Parent component using template variable. @Component({ template: ` `, imports: [TodoListComponent] }) class ParentComponent { addTodo(todoList: TodoListComponent) { // Access child component through template variable. todoList.addTodo(); } } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/4-template-variable?ref=angularspace.com). --- ## Injected Components in Angular ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/injected-component.png) Let's explore an interesting but rarely-used technique of component injection. This approach lets a child component directly access its parent by injecting the parent component into the child's constructor. While not common usage with components, it's worth understanding for specific use cases. #### 💡 Examples of Practical Uses of Injected Components 1. Complex form components where child fields need parent form context. 2. Nested menu structures where child items need parent menu state. 3. Wizard/stepper components where steps need access to the main wizard state. | Good/Bad | Description | | -------- | ------------------------------------------------------------------------------------- | | ❌ | Rare in real-world applications, which may make the code less maintainable for teams. | | ❌ | Creates strong dependencies between components, reducing reusability. | | ❌ | Limited to one-way communication from child to parent. | | ❌ | Only works with direct parent components in the hierarchy. | | ⚠️ | Rare usage with components but not with directives. | | ✅ | Simplifies parent-child communication in specific cases without extra services. | | ✅ | Provides direct access to parent methods and properties from the child component. | ```typescript // Parent component that child can access. @Component() class DialogManagerComponent { openDialog() { alert('Opening modal dialog!'); } closeDialog() { alert('Closing modal dialog!'); } } // Child component with injected parent. class DialogButtonComponent { constructor(private dialogManager: DialogManagerComponent) { this.dialogManager.openDialog(); // Direct access to parent's methods. } handleClick() { this.dialogManager.closeDialog(); } } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/5-injected-component?ref=angularspace.com). --- ## ViewChild and ViewChildren ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/view-child.png) ### Understanding ViewChild in Angular `ViewChild` is a versatile Angular tool that lets parent components interact directly with their child components. By default, it selects the first matching element or component in the view, making it perfect for one-to-one parent-child communication through the template. #### 💡 Examples of Practical Uses of `ViewChild` 1. Controlling UI components programmatically (modal dialogs, accordion panels). 2. Interacting with third-party components (maps, charts, date pickers). 3. Managing multiple similar components (tabs, carousel slides, list items). | Good/Bad | Description | | -------- | ---------------------------------------------------------------------------------------- | | ❌ | Creates tight coupling between parent and child components, which can limit reusability. | | ❌ | Limited to direct parent-child relationships only. | | ❌ | Extensive use of ViewChild can make applications harder to maintain and test. | | ✅ | Provides direct access to child component's public methods and properties. | | ✅ | Enables real-time access to child component's state and behavior. | #### Traditional approach The classic method uses the `@ViewChild()` decorator to connect a parent with its child component. You'll reference the child component's class in the decorator to establish this connection. ```typescript // Child component with a method parent can call. @Component({ selector: 'app-search-input', }) class SearchInputComponent { clearInput() { console.log('Clearing search input'); } focus() { console.log('Focusing search input'); } } // Parent component that controls the child. @Component({ selector: 'app-search-bar', template: ` `, imports: [SearchInputComponent] }) class SearchBarComponent { @ViewChild(SearchInputComponent) searchInput: SearchInputComponent; resetSearch() { this.searchInput.clearInput(); this.searchInput.focus(); } } ``` #### Modern Signal-Based Approach Angular 17.2 introduces a cleaner way to use `ViewChild` with the `viewChild()` signal function (stable from Angular 19). You can specify either a template reference variable or a component class to locate the child component. ```typescript // Parent component using signal-based ViewChild. @Component({ template: ` `, imports: [SearchInputComponent] }) class SearchBarComponent { searchInput = viewChild(SearchInputComponent); resetSearch() { this.searchInput().clearInput(); this.searchInput().focus(); } } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/6-view-child?ref=angularspace.com). --- ### Understanding ViewChildren in Angular Building on our knowledge of `ViewChild` comes its sibling feature, `ViewChildren`. This robust tool lets a parent component work with multiple child components or elements in its template. While `ViewChild` gives you one element, `ViewChildren` provides a `QueryList` containing all matching elements. #### 💡 Examples of Practical Uses of `ViewChildren` 1. Managing dynamic lists of components (todo items, form fields, list items). 2. Managing form array elements for dynamic forms. 3. Controlling multiple tab panels or accordion sections. #### Traditional Approach The classic method uses the `@ViewChildren()` decorator to access multiple child components from the parent. You'll reference the child component's class in the decorator - it's similar to `ViewChild` but gives you access to all instances instead of just one. ```typescript // Parent component that manages multiple children. @Component({ selector: 'app-tab-group', template: ` @for (tab of ['Dashboard', 'Profile', 'Settings']) { // Child components we want to access. } `, imports: [TabComponent] }) class TamGroupComponent { @ViewChildren(TabComponent) tabs: QueryList; closeAllTabs() { this.tabs.forEach(child => child.close()); } } // Child component with method that parent can call. @Component({ selector: 'app-tab', }) class TabComponent { close() { console.log('Closing tab'); } } ``` #### Modern Signal-Based Approach Angular 17+ introduces a cleaner way to use `ViewChildren` with the `viewChildren()` signal function. It works the same way but leverages Angular's reactive signal system for better performance and cleaner code. ```typescript // Parent component using signal-based ViewChildren. @Component({ selector: 'app-tab-group', template: ` @for (tab of ['Dashboard', 'Profile', 'Settings']) { // Child components we want to access. } `, imports: [TabComponent] }) class TabGroupComponent { tabs = viewChildren(TabComponent); closeAllTabs() { this.tabs().forEach(child => child.close()); } } // Child component with method that parent can call. @Component({ selector: 'app-tab', }) class TabComponent { close() { console.log('Closing tab'); } } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/7-view-children?ref=angularspace.com). --- ## ContentChild and ContentChildren in Angular ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/content-projection.png) Here's how to work with projected content in Angular components. While `ViewChild` and `ViewChildren` handle elements in a component's template, `ContentChild` and `ContentChildren` deal with content that's projected between component tags. This advanced feature helps you manage content passed from parent components. #### 💡 Examples of Practical Uses of Content Projection 1. Card component might have a predefined style and layout (like header, body, and footer areas), but the actual content of these areas can be projected by the parent component, allowing for versatile reuse across different parts of an application. 2. Tab set component where each tab’s content is projected from a parent component, allowing each tab content to be uniquely defined while using the same tab navigation system. | Good/Bad | Description | | -------- | ------------------------------------------------------------------------------------------------------------------- | | ❌ | Content is only available after the ngAfterContentInit lifecycle hook, not during initialization. | | ❌ | Component initialization cannot access or manipulate projected content. | | ❌ | Lacks strong typing, making it harder to ensure type safety for projected content. | | ⚠️ | Using multiple slots adds complexity, but enables powerful component compositions when used carefully. | | ✅ | Creates flexible and reusable components through content projection features. | | ✅ | Provides direct access to projected content, making it easy to interact with nested elements. | #### Traditional Approach Explained The classic way uses `@ContentChild()` and `@ContentChildren()` decorators along with the `` tag. This combination gives you flexible ways to project and manage content. ```typescript // Parent component with content projection slots. @Component({ selector: 'app-panel', template: `
` }) class PanelComponent implements AfterContentInit { @ContentChild('title') title: ElementRef; @ContentChildren(PanelItemComponent) items: QueryList; ngAfterContentInit() { // Access projected content after initialization. this.items.forEach(item => console.log(item.title)); } } // Child item component. @Component({ selector: 'app-panel-item', template: `
{{ text() }}
` }) class PanelItemComponent { text = signal(''); } // Example usage in a parent component. @Component({ template: `

Title Here

` }) ``` #### Modern Signal-Based Approach Angular 17+ introduces signal-based versions with `contentChild()` and `contentChildren()` functions. They work similarly, but give you the power of signals. Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/8-content-projection?ref=angularspace.com). --- ## Routing Parameters & Queries in Angular ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/router.png) ### Routing Parameters This section explores how to pass data between Angular components using route parameters. This is especially helpful when you need to share information between components that aren't directly connected in your component tree. To get started, you'll need to set up your routes in the configuration and pass them to the `provideRouter(routes)` function (or `RouterModule` if you're using the older approach). Once set up, you can pass values through these routes when navigating. Your components can then easily access these parameters. #### 💡 Examples of Practical Uses of Routing Params 1. Most common use case is navigation to detailed view of specific item. 2. Steps in multistep process or workflow, can help to keep track of the current step like `/checkout/step-2`. 3. Filtering subsections of data like `products/category/electronics`. | Good/Bad | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------------- | | ❌ | Params are always strings, so you may need to parse or convert complex data types. | | ❌ | Sensitive data passed through the URL can be visible and prone to tampering. | | ✅ | Allows passing data between components without direct parent-child relationships, enabling more flexible component interaction. | | ✅ | Data in URL params is preserved during navigation and can be shared easily through links. | | ✅ | Components can easily access params via ActivatedRoute service. | ```typescript // Sets up the routing configuration. const routes = [ { path: 'details/:id', component: DetailsComponent }, ]; const appConfig = { providers: [provideRouter(routes)] }; // Parent component handles navigation to details. @Component({ selector: 'app-product', template: ` `, imports: [RouterOutlet] }) class ProductComponent { router = inject(Router); showDetails() { this.router.navigate(['/details', '123']); } } // Child component uses the route parameter value. @Component({ selector: 'app-details', }) class DetailsComponent implements OnInit { productId = signal(''); activatedRoute = inject(ActivatedRoute); ngOnInit() { this.activatedRoute.params.subscribe(params => { // Remember to unsubscribe in real app or use `toSignal`. this.productId.set(params['id']); // Will use product id '123' from router params. }); } } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/9-routing-params?ref=angularspace.com). --- ### Routing Queries in Angular Routing queries offer a perfect solution for handling optional parameters. Unlike regular route parameters that are part of the URL path, query parameters come after a question mark (`?`) in your URL. For example: `localhost:4200/table?sort=asc`. They're great for handling things like sorting, filtering, or page numbers. You can add, change, or remove query parameters without changing your main route path. Your components can then read these parameters to adjust what they show or how they behave. #### How Are Query Parameters Different from Route Parameters? - Let's take a look on URL structures: - Route parameters: `/details/123` - Query parameters: `/details?id=123&sort=name&order=asc` #### 💡 Examples of Practical Uses of Routing Queries 1. Filtering and Sorting, e.g. list view data are common uses for query parameters. 2. Pagination - query parameters can be used to store the current page number. 3. Search terms - useful for any application that has a search feature, enhancing user experience by allowing direct navigation to pre-searched results. 4. Pre-populating forms through link, query parameters can carry the necessary data to populate form fields. | Good/Bad | Description | | -------- | --------------------------------------------------------------------------------------- | | ❌ | Can only handle string data, complex data types need parsing or conversion. | | ❌ | Sensitive data is exposed in the URL, making it vulnerable to tampering. | | ❌ | Handling large or nested data with query params can become messy. | | ❌ | Browser URL length limits restrict passing large data sets via query params. | | ❌ | Not suitable for real-time communication, only for passing state during navigation. | | ✅ | Easy to share application state across users or sessions. | | ✅ | Persist in the URL, allowing bookmarking and sharing links with current state. | | ✅ | Ideal for optional, changeable data that doesn't define the route. | | ✅ | Can pass multiple key-value pairs in a single URL, making it flexible for data sharing. | ```typescript // Parent component handles navigation to details page. @Component({ selector: 'app-product', template: ` `, }) class ProductComponent { router = inject(Router); showDetails() { this.router.navigate(['/details'], { queryParams: { id: '123', name: 'John', role: 'Developer' } }); } } // Child component displays the query parameter values. @Component({ selector: 'app-details', template: `

ID: {{ id() }}

Name: {{ name() }}

Role: {{ role() }}

`, }) class DetailsComponent implements OnInit { route = inject(ActivatedRoute); queryParams = toSignal(this.route.queryParams, { initialValue: {} as Params }); id = computed(() => this.queryParams()['id'] || ''); name = computed(() => this.queryParams()['name'] || ''); role = computed(() => this.queryParams()['role'] || ''); } ``` Full set of examples around this topic you can find in the [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/10-routing-queries?ref=angularspace.com). --- ### Using `withComponentInputBinding()` for Easier Routing Angular 16+ introduced a game-changing feature that does exactly that! Let's explore how `withComponentInputBinding()` makes routing and data handling much smoother. This new approach creates a direct connection between your URL parameters and component `Input`. It's like having an automatic pipeline that connects your URLs to your components, saving you from writing extra code! To use this feature, you'll need the `withComponentInputBinding()` function from the Angular router. Once set up, the router will automatically connect your URL parameters to your component `Input` when someone visits a page. #### 💡 Examples of Practical Uses of Routing `Input` Binding 1. Product detail pages with product information in route (e.g. `/products/:productId`) 2. Blog post pages with slug parameters (e.g. `/blog/:category/:slug`) | Good/Bad | Description | | -------- | ----------------------------------------------- | | ❌ | Can make routing more complex if used too much. | | ❌ | Not great for complex data that changes often. | | ❌ | Data only flows one way. | | ❌ | Types aren't checked automatically. | | ⚠️ | It can only be used with the routed components. | | ✅ | Clean, organized route setup. | | ✅ | Components can talk through routes. | | ✅ | Less code needed to implement simple solution. | ```typescript // Parent component handles navigation. @Component({ template: ` `, imports: [ ProductDetailsComponent, RouterOutlet ], }) class ProductListComponent { router = inject(Router); viewProductDetails(productId: string) { this.router.navigate(['/product', productId]); } } // Child component receives the productId. @Component({ template: ` Product ID: {{ productId() }} `, }) class ProductDetailsComponent { // Updates to '155' when you click the button in the parent. productId = input(''); } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/11-routing-input?ref=angularspace.com). --- ### Routing State Object When you perform navigation actions in Angular, you can also pass along a state object in the navigation extras. This object is transient, meaning it is only available during the lifetime of the navigation and does not persist if the page is reloaded. You can pass the state object using the `navigate()` method of the `Router` service, or through a `[routerLink]` directive with binding. Once you navigate to the destination component, you can access the state from the `Router` service. This is typically done in the `ngOnInit` lifecycle hook or directly in the constructor, depending on when you need to access the data. #### 💡 Examples of Practical Uses of State Objects 1. Pre-populating - if you navigate to a form component, and you want pre-populate it with data from the previous component, you can pass this data through the state object. 2. Confirming actions - if a user performs an action, and you need to pass results or confirmation messages to the next component, you can use the state object. 3. Avoid secure data in URL - if you have sensitive data that you don't want to expose in the URL, you can pass it through the state object. | Good/Bad | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ❌ | Does not work properly with SSR, because it loses the state. | | ❌ | Impossible to share a link to a specific application state with another user. | | ❌ | State object is not inherently type-safe by default. | | ⚠️ | Data passed in the state object is not retained after a refresh or if the navigation history is modified. | | ⚠️ | Actually, it's possible to pass data via URL and retrieve it even after a refresh (it depends how object is created), but in this example, we don't want to add anything more to the URL. We did it in previous topics about routing communication via queries or params. | | ✅ | Ability to pass complex data objects between components during navigation. | | ✅ | The router state object allows you to pass sensitive or personal data between components without exposing it in the URL | ```typescript // Parent component navigate to next component. @Component({ template: ` `, }) class ProfileSummaryComponent { router = inject(Router); changeRoute() { this.router.navigate(['profile-details'], { state: { userProfile: { name: 'JohnDoe', memberSince: 2020 }}}); } } // Child component receives the state object. @Component() class ProfileDetailsComponent { router = inject(Router); constructor() { this.router.events.pipe( filter(e => e instanceof NavigationStart), map(() => this.router.getCurrentNavigation()?.extras.state), ).subscribe(profileData => { // Remember to unsubscribe in real app or use `toSignal`. if (profileData) { console.log('Received profile data:', profileData); } }); } } ``` Full set of examples around this topic you can find [here](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app/12-routing-object?ref=angularspace.com). --- ## Outro That's it, you finally reached to the end of this blog post. We've covered all the ways of component communication in Angular, showed cases for "old" syntax and most recent with usage of signals. Remember that all the examples are available in the [GitHub repository](https://github.com/michalgrzegorczyk-dev/angular-component-communication/tree/master/src/app?ref=angularspace.com). I hope you found this guide helpful! Feel free to leave a comment below with any questions, or if you encounter any errors in the code, please open an issue on [GitHub](https://github.com/michalgrzegorczyk-dev/angular-component-communication/issues/new?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-11-13-at-15.jpg) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/angular-university-banner-4--1--1.jpg) ### Advanced RxJs Operators You Know But Not Well Enough pt 2. URL: https://www.angularspace.com/advanced-rxjs-operators-you-know-but-not-well-enough-pt-2/ Last updated: 2024-12-12T15:41:28.000Z In June 2024, I published an article [Advanced RxJs Operators You Know But Not Well Enough](https://dev.to/krivanek06/advanced-rxjs-operators-you-know-but-not-well-enough-1ela?ref=angularspace.com), which received significant attention and many of you found it useful. Since RxJS remains essential in Angular and has a vast number of operators, I decided to create a part 2 of this article to highlight some operators, their combinations, and practical use cases where they can be applied. In this article, we will explore the following operators: - forkJoin() vs combineLatest() - auditTime() vs debounceTime() - pairwise() - raceWith() - iif() - defer() ## RxJS: forkJoin() vs combineLatest() Both of these operators deliver the last emitted value from multiple Observables. The [combineLatest](https://www.learnrxjs.io/learn-rxjs/operators/combination/combinelatest?ref=angularspace.com) operator emits an array of the most recent values from all Observables, only when every Observable has already emitted at leas one value. It combines the latest emitted values from multiple Observables, whenever any observable emits a new value. Keep in mind that sometimes we can subscribe to a cold observable that has already emitted, and our `combineLatest()` will never emit. ```TS combineLatest([ this.stockPrice$, // emits stock prices this.exchangeRate$ // emits exchange rates ]).subscribe(([stockPrice, exchangeRate]) => { // will be logged every time any of the above observables emits console.log(`Price: ${stockPrice}, Rate: ${exchangeRate}`); }); ``` The [forkJoin](https://www.learnrxjs.io/learn-rxjs/operators/combination/forkjoin?ref=angularspace.com) operator waits for all observables to complete, then emits a single array containing the last emitted value from each Observable. If at least one Observable errors or returns `EMPTY` (completes without a value), `forkJoin()` will also throw an error or return `EMPTY`. You may have heard that `forkJoin` is very similar how `Promise.all()` works, as both emit only once when all operations complete. ```TS forkJoin({ userProfile: this.api.getUserProfile(), userSettings: this.api.getUserSettings(), userPreferences: this.api.getUserPreferences() }).subscribe(({ userProfile, userSettings, userPreferences }) => { // will be logged only once, when all of the observables emits console.log(userProfile, userSettings, userPreferences); }); ``` One mistake that occasionally happens is that a WebSocket connection is used inside a `forkJoin` operator. You want to avoid doing that because `forkJoin` waits for all its Observables to complete before emitting a value. However, WebSocket-based observables are typically designed to emit values continuously (hot Observables) and never complete unless explicitly unsubscribed. ```TS forkJoin({ // API call (completes after fetching data) apiData: this.http.get('/api/data'), // WebSocket connection (never completes) websocketData: this.websocketService.getUpdates() }).subscribe(result => { // will NEVER be logged console.log('Result:', result); }); ``` ## RxJS: auditTime() vs debounceTime() For me, these two operators have always been confusing because they are similar, but subtle distinctions make all the difference. When using [debounceTime](https://rxjs.dev/api/operators/debounceTime?ref=angularspace.com), it delays emitting a value from the source observable until there is a "pause" in the emissions for a specified duration. Use it when you want to wait for the user or event to "settle" before taking action. On the other hand, [auditTime](https://rxjs.dev/api/operators/auditTime?ref=angularspace.com) samples the source observable at regular intervals and emits the most recent value from the source observable at the end of each interval. Use it when you need periodic updates while some action is still running. You are most likely already used to use `debounceTime` on input fields, however `auditTime` may be more useful when tracking resizing or scrolling behavior. Here is an example demonstrating the difference in behavior between these two operators when tracking window resizing. Notice that `auditTime` is emitting values while the user is resizing the window, but `debounceTime` only emits when the user pauses his action. ```TS // this will emit periodically fromEvent(window, 'resize') .pipe( auditTime(500), map(() => [window.innerWidth, window.innerHeight]) ).subscribe((dimensions) => { console.log(`AUDIT TIME:`, dimensions); }); // this emits only when use stops the resizing fromEvent(window, 'resize') .pipe( debounceTime(500), map(() => [window.innerWidth, window.innerHeight]) ).subscribe((dimensions) => { console.log(`DEBOUNCE TIME:`, dimensions); }); ``` ![RxJS debounceTime() and auditTime() visual difference](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/auditTime-vs-debounceTime.gif) > ***NOTE:*** A helpful utility you can create using closures (Injection token) is a function that returns a signal to listen for window resizing events: ```TS export const WINDOW_RESIZE_LISTENER = new InjectionToken('Window resize listener', { factory: () => { const windowRef = inject(WINDOW); return toSignal( fromEvent(windowRef, 'resize').pipe( auditTime(300), map(() => windowRef.innerWidth), startWith(windowRef.innerWidth), takeUntilDestroyed(), ), { initialValue: windowRef.innerWidth }); }, }); ``` And you can use this injection token inside a component at follows ```TS windowResize = inject(WINDOW_RESIZE_LISTENER); // ^^ this is a signal ``` Other examples of using `auditTime()` include listening for game inputs or live data streams, events that never stop, where periodic logic execution is desired. ## RxJS: pairwise() The [pairwise](https://rxjs.dev/api/operators/pairwise?ref=angularspace.com) rxjs operator is a transformation operator that emits the previous and current values from an observable as a pair `[previous, current]`. This is useful when you need to compare consecutive values emitted by a source observable. A useful example might be tracking router changes for navigation improvements. ```TS import { Router, NavigationEnd } from '@angular/router'; import { filter, pairwise } from 'rxjs/operators'; this.router.events.pipe( // filter for NavigationEnd events filter(event => event instanceof NavigationEnd), // pair consecutive route navigation events pairwise() ).subscribe(([previous, current]: [NavigationEnd, NavigationEnd]) => { console.log('Previous URL:', previous.url); console.log('Current URL:', current.url); }); ``` Another common example is tracking which fields have changed in a form structure. ```TS @Component({ imports: [ReactiveFormsModule], }) export class FormTrackerComponent { myForm = inject(FormBuilder).nonNullable.group({ name: '', email: '', }); constructor() { // track changes in the form this.myForm.valueChanges .pipe( // start with the initial form state startWith(this.myForm.value), // get the previous and current form values pairwise(), // get changed fields map(([prev, curr]) => this.getChangedFields(prev, curr)), // filter only distinct field keys scan((acc, curr) => [...new Set([...acc, ...curr])], [] as string[] ) ) .subscribe((fieldChange) => { console.log('Changed fields:', fieldChange); }); } /** * identify which fields have changed between two states. * @returns - name of the field (name, email, age) */ private getChangedFields(previous: any, current: any): string[] { return Object.keys(current).filter( (key) => previous[key] !== current[key] ); } } ``` ## RxJS: race() The `race()` operator subscribes to multiple observables and emits values from the observable that emits first (and keep listening on), canceling all other subscriptions. ![RxJS race operator visual description](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/race.png) I personally haven’t used the [race()](https://rxjs.dev/api/index/function/race?ref=angularspace.com) operator so often, but lately I bumped into a scenario, where it could be considered to be used. Let’s say you are making an API request to an endpoint that is problematic, meaning the request might be stuck in the `pending` state and never resolve (with error or success response). Ideally, you want to wait for a certain period, and if the request is still `pending`, cancel it and show an error message. There are many different solutions for this, but the `race()` operator usage is one that I thought of, here is an example: ```TS @Component({ /* ... */ }) export class App { #userAPIService = inject(UserAPIService); displayItems = toSignal(race( this.#userAPIService.getUsers().pipe( map((data) => ({ status: 'loaded' as const, data})) ), of({ status: 'failed' as const,}).pipe(delay(2000)) // ^^ emit failed status if no response after 2s ).pipe(startWith({ status: 'loading' as const})), { initialValue: { status: 'loading' } } ); eff = effect(() => console.log(this.displayItemsSignal())); } ``` the `displayItem` signal has the immediate value of `{status: 'loading'}` , so you can display the loading state on the UI and then either the API `getUsers()` call is resolved or if the call is still in the `pending` state for more than `2s` then the `{status: 'error'}` value will be emitted and the API call will be cancelled. Maybe `race()` is a more complex operator for this use case and you want to consider using [timeout()](https://rxjs.dev/api/operators/timeout?ref=angularspace.com) operator for the same example. You would end up with: ```TS displayItemsSignal = toSignal( this.userAPIService.getUsers().pipe( map((data) => ({ status: 'loaded' as const, data, })), startWith({ status: 'loading' as const, }), timeout({ each: 1000, with: () => of({ status: 'failed' as const }), }) ), { initialValue: { status: 'loading' } }); ``` ## RxJS: defer() The [defer()](https://rxjs.dev/api/index/function/defer?ref=angularspace.com) operator allows you to create a new observable on demand. The observable logic is executed only when it is subscribed to, making it evaluated lazily. I personally haven’t seen it being used often, however it may be a good addition to your project if you use a lot of Promises. An example may be that let’s say you have a service that makes API calls, however instead of using the [httpClient](https://angular.dev/api/common/http/HttpClient?ref=angularspace.com) and returning an Observable, it returns a Promise. ```TS @Injectable({ providedIn: 'root' }) export class UserAPIService { #data = [{ name: 'user1' }, { name: 'user2' }, /*...*/]; getUsersPromise(): Promise { return new Promise((res) => setTimeout(() => { res(this.#data); }, 200) ); } } ``` Now, you want to display a checkbox, and once the checkbox is clicked, you want to load the users (make an API call). You googled how to [convert Promises into Observables](https://stackoverflow.com/questions/39319279/convert-promise-to-observable?ref=angularspace.com), and you know you have to use the `from` operator for that, so you end up with a code like the following: ```TS @Component({ imports: [ReactiveFormsModule, AsyncPipe], template: ` @if(control.value){ @for(item of displayItems$ | async; track item.name){ {{ item.name }} } } `, }) export class App { control = new FormControl(false, { nonNullable: true }); displayItems$ = from(this.inject(UserAPIService).getUsersPromise()); } ``` I haven’t found this information in the [rxjs from() docs](https://rxjs.dev/api/index/function/from?ref=angularspace.com), however when you use `from()`, it will immediately convert Promises into Observables, which means, the `getUsersPromise()` is executed eagerly, even before the checkbox is clicked. This behavior may not be a huge drawback, since you want to load the user data regardless. It just depends on the use case whether eager loading is a desired behavior. If you want to wait until the subscription is hit (when the checkbox is clicked) you can use `defer` such as ```TS displayItems$ = defer(() => from(this.userAPIService.getUsersPromise())) ``` Using [defer()](https://stackoverflow.com/a/38771306?ref=angularspace.com), it delays the `getUsersPromise()` execution (making the API call) only when subscription happens. ## RxJS: iif() There are many use cases when we listen to the emitted values of an Observable and use `switchMap` (or another [higher order observable](https://dev.to/krivanek06/angular-interview-what-is-higher-order-observable-2k03?ref=angularspace.com)) with a condition to determine what to return. Here is one example: ```TS displayItemsSignal = toSignal( this.checkboxControl.valueChanges.pipe( switchMap((isChecked) => isChecked ? this.userAPIService.getUsers() : this.groupAPIService.getGroups() ) ), { initialValue: [] }); ``` Although this example works fine, you can use the `iif()` operator for syntax sugar if both the `getUsers()` and `getGroups()` return an Observable. ```TS displayItemsSignal = toSignal( this.checkboxControl.valueChanges.pipe( switchMap((isChecked) => iif( () => isChecked, this.userAPIService.getUsers(), this.groupAPIService.getGroups() // ^^ both return an Observable of items ) ) ), { initialValue: [] }); ``` There is one catch however. Let’s say, that instead of Observables, the service is using Promises for the data retrieval: ```TS @Injectable({ providedIn: 'root' }) export class UserAPIService { #data = [{ name: 'user1' }, { name: 'user2' }, /*...*/]; getUsersPromise(): Promise { return new Promise((res) => setTimeout(() => { console.log('UserAPIService resolved'); res(this.data); }, 200) ); } } @Injectable({ providedIn: 'root' }) export class GroupAPIService { #data = [{ name: 'group1' }, { name: 'group2' }, /*...*/]; getGroupPromise(): Promise { return new Promise((res) => setTimeout(() => { console.log('GroupAPIService resolved'); res(this.data); }, 200) ); } } ``` and if you are using the `iif()` operator with Promises, such as below ```TS displayItemsSignal = toSignal( this.checkboxControl.valueChanges.pipe( switchMap((isChecked) => iif( () => isChecked, this.userAPIService.getUsersPromise(), this.groupAPIService.getGroupPromise() // ^^ both return a Promise of items ) ) ), { initialValue: [] }); ``` What will happen is that whether the checkbox is checked, or not, both methods, the `getUsersPromise()` and the `getGroupPromise()` are executed. Definitely not what we desired. ![RxJS iif() operator with Promises](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/iif-without-defer.gif) You can fix this problem with 2 solutions. First is going back to the ternary operator such as ```TS displayItemsSignal = toSignal( this.checkboxControl.valueChanges.pipe( switchMap((isChecked) => isChecked ? this.userAPIService.getUsersPromise() : this.groupAPIService.getGroupPromise() ) ), { initialValue: [] }); ``` this fixes the problem when you are working with Promises, however, if you want to use the `iif()` operator, then you should also use the `defer()` operator, to avoid eager execution, like: ```TS displayItemsSignal = toSignal( this.checkboxControl.valueChanges.pipe( switchMap((isChecked) => iif( () => isChecked, defer(() => this.userAPIService.getUsersPromise()), defer(() => this.groupAPIService.getGroupPromise()) // ^^ delay the promise execution only if subscription happens ) ) ), { initialValue: [] }); ``` ![RxJS iif() operator with Promises using defer](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/iif-with-defer.gif) To recap, you can freely use the `iif()` conditional operator when working with Observables. However, when you are working with Promises, either use the [Conditional (ternary) operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional%5Foperator?ref=angularspace.com) or combine `iif()` with the `defer()` operator. ## Summary As the continuation to the first article, I tried to pick operators the either may cause some confusion or they can be useful in very specific situations. I hope you liked the article and feel free to share your thoughts, or connect with me on [dev.to](https://dev.to/krivanek06?ref=angularspace.com) | [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-06-24-at-15.46.39.jpg) ### Day 4&5/24 Angular Christmas Calendar URL: https://www.angularspace.com/day-4-5-24-angular-christmas-calendar/ Last updated: 2024-12-05T13:16:09.000Z Angular Christmas Calendar - Day 4&5/24 = UNLOCKED 🎅🎄☃️ I don't want to spam your email - so i'm going to post every 2 days! Day 3/24 ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-04-at-14.29.00.png) Day 4/24 ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-05-at-14.05.18.png) --- Each day you MAY find hidden - Free E-books - Free Courses - Various Discounts - \+ more! It will be first come first serve basis, so make sure to keep checking the website each new day :) + it will require some thinking to acquire gifts. ( AI WONT HELP! 🕶️ ) [Angular Christmas CalendarThe Angular Christmas Calendar website - 24 guests will share their personal insights on Angular as seen in 2024.![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/icon/favicon-8.ico)![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/thumbnail/angular-christmas-calendar.png)](https://angularchristmascalendar.com/?ref=angularspace.com) ### Day 3/24 Angular Christmas Calendar URL: https://www.angularspace.com/day-3-24-angular-christmas-calendar/ Last updated: 2024-12-03T14:11:48.000Z Angular Christmas Calendar - Day 3/24 = UNLOCKED 🎅🎄☃️ Day 3/24 is live since couple of hours :) Thank you [](https://www.linkedin.com/in/ACoAAAVdATQBGxAEi6cNU7BFbtdbFikhzxNV16U?ref=angularspace.com)[Brecht Billiet](https://www.linkedin.com/in/brecht-billiet-58417426/?ref=angularspace.com) for sponsoring Day 2 with 10 FREE Books Building AI Agents in Nx Huge thanks as well to [](https://www.linkedin.com/in/ACoAAA4PefUBJ3Q7iNFzzOxjyAiOM%5Fq6ktz05og?ref=angularspace.com)[Tomas Trajan](https://www.linkedin.com/in/tomastrajan/?ref=angularspace.com) for your Day 2 video !! All books have been claimed yesterday inside of the Calendar :) Checkout Brecht Book and Nitrokit! 👇 Book: [https://www.simplified.courses/build-ai-agents](https://www.simplified.courses/build-ai-agents?ref=angularspace.com) Nitrokit: [Nitrokit - Ship Angular apps faster with AI, Supabase and TailwindNitrokit will help you ship your applications ridiculously FAST!![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/icon/apple-touch-icon-2.png)Nitrokit - Ship Angular apps faster with AI, Supabase and Tailwind![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/thumbnail/card-1.jpg)](https://nitrokit.ai/?ref=angularspace.com) --- Each day you MAY find hidden - Free E-books - Free Courses - Various Discounts - \+ more! It will be first come first serve basis, so make sure to keep checking the website each new day :) + it will require some thinking to acquire gifts. ( AI WONT HELP! 🕶️ ) [Angular Christmas CalendarThe Angular Christmas Calendar website - 24 guests will share their personal insights on Angular as seen in 2024.![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/icon/favicon-8.ico)![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/thumbnail/angular-christmas-calendar.png)](https://angularchristmascalendar.com/?ref=angularspace.com) ### Day 2/24 Angular Christmas Calendar URL: https://www.angularspace.com/day-2-24-angular-christmas-calendar/ Last updated: 2024-12-02T13:42:40.000Z Angular Christmas Calendar - Day 2/24 = UNLOCKED 🎅🎄☃️ Day 1 started with an amazing video from Muhammad Ahsan Ayaz !! Thank you so much for taking part in this 🙂 Who might be the second guest? Maybe gifts today as well? 🙂 Each day you MAY find hidden - Free E-books - Free Courses - Various Discounts - \+ more! It will be first come first serve basis, so make sure to keep checking the website each new day :) + it will require some thinking to acquire gifts. ( AI WONT HELP! 🕶️ ) [Angular Christmas CalendarThe Angular Christmas Calendar website - 24 guests will share their personal insights on Angular as seen in 2024.![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/icon/favicon-8.ico)![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/thumbnail/angular-christmas-calendar.png)](https://angularchristmascalendar.com/?ref=angularspace.com) ### Day 1/24 Angular Christmas Calendar URL: https://www.angularspace.com/day-1-24-angular-christmas-calendar/ Last updated: 2024-12-02T13:43:05.000Z It's here! Day 1 Unlocked :) Who might be the first guess? Make sure to visit the website. Additionally, on some days you are going to find hidden FREE gifts such as: - Free E-books - Free Courses - Various Discounts - \+ more! It will be first come first serve basis, so make sure to keep checking the website each new day :) + it will require some thinking to acquire gifts. ( AI WONT HELP! 🕶️ ) 0:00 /0:10 1× [Angular Christmas CalendarThe Angular Christmas Calendar website - 24 guests will share their personal insights on Angular as seen in 2024.![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/icon/favicon-8.ico)![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/thumbnail/angular-christmas-calendar.png)](https://angularchristmascalendar.com/?ref=angularspace.com) ### Angular v19 No Signals Edition URL: https://www.angularspace.com/angular-v19-no-signals-edition/ Last updated: 2024-11-29T15:45:38.000Z Angular v19 is all the rage these days! Of course, everyone talks about SSR improvements, like incremental hydration, `linkedSignal` and `resource`/`rxResource` APIs, and you can already find some articles about them in here: - [Article on Resource API](https://www.angularspace.com/everything-you-need-to-know-abour-resource-for-now/) by myself - Article about [porting the Resource API to older version using](https://www.angularspace.com/creating-custom-rxresource-api-with-observables/#comments) Observables by Eduard Krivanek But today, we are going to relax a bit from all the hype and talk about smaller, somewhat overlooked additions and improvements in Angular v19 that might have slid under the radar for the past week. So, without further ado, let's dive in into the no-signals edition! ## Improved Application initializer developer experience Sometimes in Angular apps, we need to run a certain functions before the application, and everything inside, start running. This might be a function to fetch mission critical configurations, or maybe some logic to hydrate previous application state from `localStorage`. Regardless of the purpose, the ability to run such functions is important, and before v19, we used the `APP_INITIALIZER` token to do so: ```typescript export function initializeApp(configService: ConfigService) { return () => configService.loadConfig();   } export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), ConfigService, { provide: APP_INITIALIZER, useFactory: initializeApp, deps: [ConfigService], multi: true } ] }; ``` This worked well, but looks a bit boilerplate-y. To improve this, Angular has introduced a new `provideAppInitializer` function that can be used to register such functions: ```typescript import { provideAppInitializer } from '@angular/core'; export function initializeApp() { const configService = inject(ConfigService); configService.loadConfig(); } export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), ConfigService, provideAppInitializer(initializeApp) ] }; ``` Now, this is a lot cleaner. The provided initializer function runs in an injection context, so we can use `inject` to get the services we need. We can call the `provideAppInitializer` function multiple times to register multiple initializers. This also works with `PLATFORM_INITIALIZER` and `ENVIRONMENT_INITIALIZER` tokens, which are replaced by `providePlatformInitializer` and `provideEnvironmentInitializer` respectively. > Note: the previous `APP_INITIALIZER`, `PLATFORM_INITIALIZER` and `ENVIRONMENT_INITIALIZER` tokens are still supported, but they are deprecated and will be removed in the future. ## Router Outlet Data Components that are rendered inside other components can receive data from the parent component via `input` properties. Routed components can read URL parameters, query parameters and resolved data via `input`\-s too. This we all know, but what about scenarios where the data is coming from a component higher in the component tree and is not part of routing data (params, etc)? For instance, we might have a secondary router outlet for a sidebar, but sidebar menu items are stored in an array in the component that actually renders the router outlet? So, how we pass that data down? Thankfully, in v19, a new `input` property called `routerOutletData` is added on the `` directive, which allows passing of some data down to the components rendered via that outlet. We can simply provide that data: ```typescript @Component({ template: ` `, }) export class NavigationComponent { readonly #navigationService = inject(NavigationService); sidebarItems: NavigationData = { items: this.#navigationService.getSidebarItems(), }; } ``` Now, in any child route, in a component we can retrieve that data: ```typescript @Component({ template: ` @for (item of routerOutletData().sidebarItems) { } `, }) export class SidebarComponent { routerOutletData = inject(ROUTER_OUTLET_DATA) as Signal; } ``` As you can see, the really amazing thing about this is that the data being injected comes as a `Signal`, which means that it will be updated when it changes in parent components, allowing us seamless communication between routes, without having to resort to state management solutions or services injected everywhere. ## `typeof` operator in templates Recently, there has been much talk about introducing lexical scope to Angular component templates, meaning we could access global variables, functions, and broader JavaScript functionality like `Math` and `Date` in templates. While this particular feature is still far in the future, there has been a slight improvement to the template operations thanks to [Matthieu Riegler](https://x.com/Jean%5F%5FMeche?ref=angularspace.com), one of our top experts, who submitted a PR which allows us to use the `typeof` operator in templates. This can be useful when we have volatile data types; for instance, some third-party library might return an object as a result of an operation, and, instead of throwing an error, return an error message, leaving us with a `Result | string` type. As we do not have control over a third-party library, we are forced to write cumbersome logic to handle the error case. However, with the `typeof` operator, we can easily check the type of the result and handle it accordingly: ```html @let result = getResult(data); @if (typeof result === 'string') { Error: {{ result }} } @else { Success: {{ result.value }} } ``` This also ensures type safety directly in the template, taking our code to the next level! ## Resolvers support redirecting When writing guards, the logic is often "check something, and return true if it is ok to navigate, or return a redirect command to guide the user to a different page". This is very useful, however, it was never available to use in resolvers, where we were forced to write redirection logic by injecting the router and manually calling `navigate` or `navigateByUrl`, somewhat breaking the declarative approach to the resolver code. This, however, is about to change, because resolvers, starting from Angular v19, are going to also support `RedirectCommand` objects as return types. For example, if we have a resolver that loads a product, we can easily redirect the user to a "Not found" page if the product is not found: ```typescript export const productResolver: ResolveFn = ({ paramMap }) => { const router = inject(Router); const productService = inject(ProductService); const id = paramMap.get('id') ?? 0; return productService .getProductById(+id) .pipe( catchError(() => of(new RedirectCommand(router.createUrlTree(['/error']))), ) ); }; ``` This simple code is all that we need to perform this relatively complex action, improving the developer experience by a lot. ## Empty styles result in `ViewEncapsulation.None` In Angular, when we create a component that has its own styles, the component is wrapped in something known as `ViewEncapsulation`. This allows us to specify any styles and be assured that styles from other components that use the same selectors will not conflict with ours. This is done via adding specific attributes to the component's template elements. A rendered Angular template might look like something like this: ```html

``` As we can see, cusomt attributes like `_ngcontent-ng-c3298008605` and so on are added to the elements that Angular renders to make them unique enough so that styles applied to those don't collide with other styles created in the same application. However, if we do not have any custom styles in a component, it would make sense to set `ViewEncapsulation` to `None` so that we can avoid the overhead of adding those attributes to all the elements. Previously, if the `styles` or `styleUrls` properties were not set, Angular would default to `ViewEncapsulation.None`, but not when those properties were set without actually providing any styles, so this code: ```typescript @Component({ template: `

Hello!

`, styles: ``, }) export class AppComponent {} ``` would still render the auto-generated attributes unnecessarily, resulting in performance overhead. Considering that the Angular CLI by default generates `component.scss` files and lots of developers tend to leave them even when they are empty, this results in lots of unnecessary work by the compiler (and later by the browser when actually rendering the HTML). This behavior has been fixed in v19, and now skipping adding styles on a component will result in skipping adding the custom attributes. ## Usage of the `this` keyword in the template Usually, when we write logic in the template and refer to the component instance properties, we do *not* use the `this` keyword, because the Angular compiler knows to use the component instance, so code like `{{ title }}` will be automatically interpreted as `{{ this.title }}`. We all know this, however, it is a lesser known fact that we can also explicitly write the `this` keyword in the template, however useless that is. Although, we might think that there are two cases where that could be useful. First is when we want to extract the value of a possibly nullable signal to work around a [TypeScript issue](https://github.com/angular/angular/issues/54906?ref=angularspace.com) present in Angular templates that use signals. ```html @if(someSignal()) { {{ someSignal().someProperty }} } ``` To avoid doing this, we could extract the value of the signal to a local variable, and then use that local variable in the template, like this: ```html @let someSignalValue = someSignal() @if(someSignalValue) { {{ someSignalValue.someProperty }} } ``` However, it is a bit cumbersome to find names for all signals that behave this way, and suffixing everything with `value` is not the cleanest solution. Thankfully, we can create a local variable with the same name using the `this` keyword in the template, like this: ```html @let someSignal = this.someSignal() @if(someSignal) { {{ someSignal.someProperty }} } ``` While this functionality existed for a while now and is not new to v19, as mentioned, there is another use case for `this` in templates: when a template variable has the same name as a component property, for example, in an `ng-template` with a local variable. So, we are justified to assume that this code is going to work: ```html {{ title }} {{ this.title }} ``` But, as funny as it is, until v19, the Angular compiler in both cases assumed that the variable is referring to the local variable, and not the component property, yes, even with explicit `this`. This was a bug that was fixed in v19, so now we can easily differentiate between local variables and component class properties whenever necessary. ## APIs marked as stable As we all know all too well, Angular is in the process of reinventing itself, so many tools already available for us are yet marked as experimental or in developer preview. However, v19 is also a big release in terms of stabilizing functionality (ok, we gotta talk about signals a bit here too). Here is a table illustrating all the APIs that are now marked as stable: | API | Introduced in Version | Status | | ------------------------------------------------- | --------------------- | ------ | | @let syntax | v18.1 | Stable | | takeUntilDestroyed operator | v15 | Stable | | outputFromObservable/outputToObservable functions | v18.2 | Stable | | input/output/model properties | v18+ | Stable | | Signal-based queries (viewChild and so on) | v18+ | Stable | | withRequestsMadeViaParent option for HttpClient | v18 | Stable | This is great news for Angular enthusiasts, as it means we can now freely use all these new features in production-ready application without worrying about further breaking changes! > Note: you can use [this interactive guide](https://www.angular.courses/caniuse?ref=angularspace.com) to keep track of all new, stable, or experimental features in Angular. ## Conclusion As we have seen, Angular releases, especially major ones like v19, are always full of improvements, that sometimes get overlooked because of really hyped-up (rightfully so) features. So, with this article, I tried to cover the most important and useful ones, to help Angular developers stay up-to-date with the latest developments. You can check the official release on GitHub following [this link](https://github.com/angular/angular/releases/tag/19.0.0?ref=angularspace.com) to learn more. ## Small promotion ![Modern Angular.jpeg](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Modern-Angular.jpeg) Angular is evolving rapidly, and it is easy to find yourself in a bit overwhelmed kind of situation. Thankfully, I have a solution to this! This past 18 months, I have been working on a comprehensive Angular book! It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular?ref=angularspace.com). The book is now in the production phase with a print release scheduled in upcoming weeks, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on [Twitter](https://twitter.com/Armandotrue?ref=angularspace.com) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/?ref=angularspace.com), where I will be posting whenever there are news or promotions available. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/Screenshot-2024-11-13-at-15.52.00.jpg) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/angular-university-banner-4--1--1.jpg)](https://angular-university.io/?ref=angularspace.com) ### Angular Christmas Calendar is back! URL: https://www.angularspace.com/angular-christmas-calendar-is-back/ Last updated: 2024-11-21T16:04:48.000Z We are reactivating this fantastic community project for 2024! Each day you are going to have a new window available to personally open like you would open real life Advent Calendar. What's inside? We are going to host more then 24 Angular Experts as they share videos or write ups with: - Personal thoughts about 2024 with Angular - Optional Outlook in to 2025 - Mandatory Practical/Technical Advice so you can learn everyday - Some days are going to be double ( 2 experts ) 😄 We initially launched this project in 2023 and did a whole run. It's using Angular v17 + SSR :) Still rocking top score!! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/Screenshot-2024-11-21-at-15.43.43.png) Repo: [https://github.com/danielglejzner/angular-christmas-calendar](https://github.com/danielglejzner/angular-christmas-calendar?ref=angularspace.com) Website: [https://angularchristmascalendar.com/](https://angularchristmascalendar.com/?ref=angularspace.com) ### **Special Announcement!** [Brecht Billiet](https://www.angularspace.com/author/brecht/)has prepared a special discount for everything is his shop! [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/5dT_yWFH.jpg)](https://t.co/z9DRPjdRC1?ref=angularspace.com) ### Everything about v19 Resource API (for now) URL: https://www.angularspace.com/everything-you-need-to-know-abour-resource-for-now/ Last updated: 2024-11-20T16:06:27.000Z With the introduction of signals in Angular v16, the framework started a new path towards rethinking reactivity and change detection. Signals were a welcome and very popular addition. however, everyone understood that they are still in a raw state and more reactive primitives are needed to make them truly useful. The big difference between signals and RxJS, which is already well-supported in Angular, is that Observables are usually used for async operations (although they can be synchronous too, while signals are always synchronous). So, naturally, a question of reconciling the two has emerged, and this is how Angular got the `resource` and `rxResource` reactive primitives. In this article, we will deconstruct these tools using practical examples, so that you can become comfortable using them in your day-to-day projects. > Note: `resource` and `rxResource` are experimental, meaning that the core team does not encourage their use in production ready applications. An RFC from the Angular team is planned to discuss all the outstanding issues. The API of these functions can change drastically in the future. This article will be updated to have links to more up-to-date resources. ## The problem One of the most common tasks in front end development is to fetch some data from a server and then display it in the UI. Previously, we did something like this: ```typescript @Component({ template: `

{{data.title}}

{{data.description}}

` }) export class MyComponent implements OnInit { readonly #dataService = inject(DataService); data: SomeData = null; ngOnInit() { this.#dataService.getData().subscribe(data => this.data = data); } } ``` This more or less covered our concerns, but was verbose and not very flexible. Components that loaded lots of data had lots of disconnected pieces of code, properties declared empty only to be filled in later in `subscribe` callbacks in `ngOnInit` or elsewhere. More savvy developers overcame this problems by using the `async` pipe, which allows us to read Observable values in the template. Here is the same example rewritten using `async`: ```typescript @Component({ template: `

{{data.title}}

{{data.description}}

` }) export class MyComponent implements OnInit { readonly #dataService = inject(DataService); data$ = this.#dataService.getData(); } ``` This solves the verbosity problem, but still does not address the full scope of concerns related to data fetching. What if I want to show a loading spinner? What about errors? How about retrying, also, what if I don't want to make the request immediately, but rather wait for some value change (like a signal update) or some user event (user clicked on a button)? Very different approaches have been adopted by Angular developers, some using RxJS operators like `switchMap`, `retry`, and some adopting community-based solutions like the [derivedAsync](https://ngxtension.netlify.app/utilities/signals/derived-async/?ref=angularspace.com) function from the *ngxtension* package. However, it was high time Angular introduced an own, native tool for solving these concerns. ## The solution In Angular v19, two functions were introduced, `resource` and `rxResource`. In essence, they do the same thing, the only difference being that `resource` works with Promises, while `rxResource` works with Observables. As Angular's HTTP client is based on RxJS, let's discuss `rxResource` and keep in mind that the same things would be true if we used `fetch` instead of `HttpClient.get` and `resource` instead of `rxResource`. ### Loading data Let's build a working page using `rxResource`. We will use the Rest Countries API, which is a free API that provides detailed information about countries of the world. Let's begin simple and just build a page that displays all the countries. ```typescript export type Country = { name: { official: string; }; // other properties }; @Component({ template: ` @if (countriesResource.status() === status.Resolved) {
    @for ( country of countriesResource.value(); track country.name.official ) {
  • {{ country.name.official }}
  • }
} `, }) export class AppComponent { readonly #http = inject(HttpClient); status = ResourceStatus; countriesResource = rxResource({ loader: () => this.#http.get('https://restcountries.com/v3.1/all'), }); } ``` Let us break down what goes on here: 1. With the `HttpClient`, we fetch the list of countries. This is simple 2. The `rxResource` function accepts a configuration object, which contains a `loader` function. 3. This loader function is what will be performed to actually make the request. We can put whatever logic we want there, but mainly it would be a call to `HttpClient.get`. 4. We also import and use the `ResourceStatus` enum, which contains the possible states of a resource. 5. In the template, we first check if the resource is resolved (`countriesResource.status() === status.Resolved`). 6. Then, we use the resource to read it's values. The `value` that we use is actually a signal that will be updated when the resource is resolved. Now, the `status` signal contains valuable information about the state of the resource, however, it is not always great for determining if the data is available to display. We will see some differences later in this article, but for now, let us update the example to use another signal called `hasValue`, which will be true if the resource finished loading and the data is available to read: ```html @if (countriesResource.hasValue()) {
    @for ( country of countriesResource.value(); track country.name.official ) {
  • {{ country.name.official }}
  • }
} ``` This will handle all the outstanding cases. As we can see, everything related to this HTTP request is encapsulated in the `countriesResource` object. Let's see what else we can do with it. ### Handling errors So, how can we learn if the request failed? And how do we use the actual error message/object? Simple, we can use the `error` signal and the status again. Here is the updated code: ```html @if (countriesResource.hasValue()) {
    @for ( country of countriesResource.value(); track country.name.official ) {
  • {{ country.name.official }}
  • }
} @else if (countriesResource.status() === status.Error) { The request failed. Reason: {{ countriesResource.error() }} } ``` As we can see, we only had to update the template to handle the error case. The `ResourceRef` object produced by `rxResource` contains all the information we need to know if the request failed, and what the actual error was via the `error` signal. ### Retrying failed requests What if I want to retry? For example, I might want to show a button that will retry the request. ```html @if (countriesResource.hasValue()) { // omitted for the sake of brevity } @else if (countriesResource.status() === status.Error) { The request failed. Reason: {{ countriesResource.error() }} } ``` The `ResourceRef` has a `reload` function, which, in essence, will trigger the same `loader` function we have provided, allowing us to retry when we have an error, or just reload then request whenever we want. ### Loading State Another common concern for HTTP requests is showing some sort of loading indicator, like a spinner or at least some text. Of course, this is also easily achievable with a resource: ```html @if (countriesResource.hasValue()) { // omitted for the sake of brevity } @else if (countriesResource.status() === status.Error) { The request failed. Reason: {{ countriesResource.error() }} } @else if ( countriesResource.status() === status.Loading || countriesResource.status() === status.Reloading ) { Loading Countries... } ``` As we can see, there are two distinct statuses here, `Loading` and `Reloading`, which indicate whether it is the first time loading the data, or a load triggered later. This can be useful in some scenarios where we want to differentiate between the two. However, this is a bit cumbersome in generic scenarios, and downright problematic in scenarios where we want to be able to update the resource value manually (later in the article, we will see that this is possible), because in that case the `status` would be `ResourceStatus.Local`, indicating that the value has been tampered by some action on the user's end. To avoid the hassle of doing multiple checks, `ResourceRef` contains a simple signal called `isLoading` that clearly indicates if there is a necessity to show a loading UI. Let's refactor the code to see it in action: ``` @if (!countriesResource.isLoading()) { // omitted for the sake of brevity } @else if (countriesResource.status() === status.Error) { The request failed. Reason: {{ countriesResource.error() }} } @else { Loading Countries... } ``` Now, before we move on, let's think what would be different if we, for whatever reason, wanted to use completely ditch RxJS and use promises instead. Well, we could not use `HttpClient` anymore, obviously, as it always returns Observables, and instead of `rxResource` we would use `resource`: ```typescript export class AppComponent { readonly #http = inject(HttpClient); status = ResourceStatus; countriesResource = resource({ loader: () => { // throw new Error('X'); return fetch('https://restcountries.com/v3.1/all') .then(res => res.json()) as Promise; }, }); } ``` Very simple change, and, most notably, everything else remains the same. Because of that, and the fact that most Angular codebases use `HttpClient` (and hence RxJS) for HTTP requests, further down the article we will just use `rxResource` and you can safely assume that everything we say about it is also true for `resource`. Now, let us move on to more complex cases. ## Re-evaluating loaded data based on other signals One very common concern is filtering, sorting or paginating the data fetched from a server. Usually, we have some inputs which the user can interact with to set the page, sorting parameters and so on, and based on those changes, we want to reload the data. This can easily be achieved with `rxResource`, as it accepts another configuration parameter, a function that returns the request parameters to be used. Let's imagine we are adding a search input to our page, so we can filter the countries by their name. ```typescript @Component({ template: ` @if (!countriesResource.isLoading()) {
    @for ( country of countriesResource.value(); track country.name.official ) {
  • {{ country.name.official }}
  • }
} @else if (countriesResource.status() === status.Error) { The request failed. Reason: {{ countriesResource.error() }} } @else { Loading Countries... } `, }) export class AppComponent { readonly #http = inject(HttpClient); countryName = signal(''); status = ResourceStatus; countriesResource = rxResource({ request: () => ({ name: this.countryName() }), loader: (parameters) => { return this.#http.get( `https://restcountries.com/v3.1/name/${parameters.request.name}`, ); }, }); } ``` There are two changes here; first, we added a `countryName` signal and bound it to the input with `[(ngModel)]`. Next, we added a `request` function to the resource configuration object. So what this function does is compute parameters for the request, and if we use signals in this function, it will be re-evaluated whenever the signal changes. The signals in that callback are tracked just as they are tracked in `computed` of `effect`. This means, that we described a whole lifecycle in a simple line of code: 1. User types the name of the country in the input 2. The signal is updated via `[(ngModel)]` 3. This triggers the re-evaluation of the `request` function 4. This triggers the re-evaluation of the `loader` function, which makes a new HTTP call based on new parameters 5. This triggers updates to the `countriesResource` object, which will change its status to `Loading` (show the loading text/spinner/whatever) and then to `Resolved`/`Error` depending on the outcome of the request All of this happens with us only providing a simple function that describes what reactive parameters our request is dependent on. Also, if we are using `resource` instead of `rxResource` (thus using `fetch`), the first argument of the `request` function (which we named `parameters` in the example) will also contain an abort signal, which we can use to cancel the request if needed! ```typescript export class AppComponent { readonly #http = inject(HttpClient); countryName = signal(''); status = ResourceStatus; countriesResource = resource({ request: () => ({ name: this.countryName() }), loader: (parameters) => { // throw new Error('X'); return fetch( `https://restcountries.com/v3.1/name/${parameters.request.name}`, {signal: parameters.abortSignal} ).then(res => res.json()) as Promise; }, }); } ``` When we do this, we can then simply use a specific `ResourceRef` method to cancel the request: ```html ``` This abort signal is available both in `resource`, and `rxResource`, however, in the case of `rxResource` it is not necessary for cancellation purposes and can be ignored. With `rxResource`, we can still call `destroy` on the `ResourceRef` object returned by it, in which case it will simply unsubscribe from the Observable that we returned, thus cancelling the request. ### Some caveats However, if we run this exact code (or the previous example with `resource`) with this specific API URL, we will first encounter an error. This is because the API we are using expects a `name`, and initially it is an empty string, so the request will fail. So how do we handle this? This is also important in a context where we don't want to make the request immediately, but rather wait for some value change, like this very example. Well, in our simple case, we can just check if the `name` parameter has any value, and if not, return an Observable of an empty array: ```typescript export class AppComponent { readonly #http = inject(HttpClient); countryName = signal(''); status = ResourceStatus; countriesResource = rxResource({ request: () => ({ name: this.countryName() }), loader: (parameters) => { if (parameters.request.name === '') { return of([]); } return this.#http.get( `https://restcountries.com/v3.1/name/${parameters.request.name}` ); }, }); } ``` > Note: with `resource` we can use `Promise.resolve([])` instead of `of([])` Furthermore, if we want to do some specific comparison, the `parameters` object also contains a `previous` property, which contains the previous request parameters. This can be useful in scenarios with many source signals, for example, when we want to only reload the data if the user has changed a specific value, but not the others. ## Using the data locally Another great aspect of `rxResource`/`resource` is that they contain writable signals, which means we can modify them or bind them to inputs using `[(ngModel)]`. This is super useful for a very common scenario of loading some data from the server so that the user can then edit it. Here is a short, generic example: ```typescript export class SomeComponent { readonly #http = inject(HttpClient); someValueResource = resource({ loader: () => this.#http.get('https://some-url.com'), }); updateSomeValue() { this.someValue.set('Some new value'); } } ``` As we can see, `ResourceRef` is a writable signal, and we can use it to update the value of the resource outside of just reloading it from the server. We can upgrade our example to add logic that allows the user to delete a country by clicking on an "X" button next to it: ```typescript @Component({ selector: 'app-root', standalone: true, template: ` @if (!countriesResource.isLoading()) {
    @for ( country of countriesResource.value(); track country.name.official; ) {
  • {{ country.name.official }}
  • }
} @else if (countriesResource.status() === status.Error) { The request failed. Reason: {{ countriesResource.error() }} } @else { Loading Countries... } `, imports: [FormsModule], }) export class AppComponent { readonly #http = inject(HttpClient); countryName = signal(''); status = ResourceStatus; countriesResource = rxResource({ request: () => ({ name: this.countryName() }), loader: (parameters) => { if (parameters.request.name === '') { return of([]); } return this.#http.get( `https://restcountries.com/v3.1/name/${parameters.request.name}` ); }, }); deleteCountry(name: string) { this.countriesResource.update((countries) => { return countries?.filter( (country) => country.name.official !== name ); }); } } ``` So, like any other signal, we can call `update` on the `ResourceRef` object and, in this case, filter out the country we want to delete. However, this is particularly useful in conjunction with template-driven forms. Because it is now possible to bind signals to inputs via `[(ngModel)]`, we can simply load the data that the user will be able to edit, and then bind it directly to some input: ```typescript @Component({ standalone: true, template: `
`, }) export class ChangeEmailComponent { readonly #userService = inject(UserService); emailResource = rxResource({ loader: () => this.#userService.getUserEmail(), }); } ``` As we can see, we simply do a binding to the resource itself - eloquent and simple! This can also be coupled with the new `linkedSignal` primitive to produce complex forms with multiple inputs: ```typescript @Component({ standalone: true, template: `
`, }) export class EditUserProfileComponent { readonly #userService = inject(UserService); userResource = rxResource({ loader: () => this.#userService.getUser(), }); form = { firstName: linkedSignal(() => this.userResource.value()?.firstName ?? ''), lastName: linkedSignal(() => this.userResource.value()?.lastName ?? ''), email: linkedSignal(() => this.userResource.value()?.email ?? ''), }; } ``` Now, we can create a form that can be both modified by the user *and* updated from HTTP requests if necessary, all the while keeping the amount of boilerplate to a minimum. ## Important to know First of all, it is important to understand that both functions might yet still evolve, and, as mentioned above, they are still in developer preview. One significant thing missing right now is switch-control; currently, the requests performed by `rxResource`/`resource` will switch from a previous request to a new one when source signals change. This means that previous requests will be canceled to make room for new ones. This is the most common behavior, however sometimes we might want to actually make several requests in parallel, or wait and perform all of them sequentially. We could easily do that with plain RxJS operators like `mergeMap` or `concatMap`, but with resources we are still a bit limited. It remains an open question if the team will later update this API to include choices for switching behaviors. Finally, and this is very important, as the documentation says, the `rxResource` and `resource` functions are only meant to be used for getting data, and not for POST/PUT/DELETE. So, do **not** use this to edit data, delete items or submit forms. ## Simplified API reference Here I also want to include some tables showing the simplified API of `rxResource` and `resource` (both return `ResourceRef`, so only one table per concern) so you can easily recap what we talked about. Feel free to skip this if you want to try it out yourself, and feel free to get back to it later to clarify nuances. ### `ResourceStatus` enum This enum contains the possible values of the `status` signal of a `ResourceRef`. | ResourceStatus | Description | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Idle | Status is Idle when either the resource has been destroyed manually, or it has not yet performed its very first request | | Error | Loading failed with an error. | | Loading | The resource is currently loading a new value as a result of a change in its request. This status happens when the source signals change, but not when we manually call ResourceRef.reload() | | Reloading | The resource is currently reloading a fresh value for the same request. This status happens when we manually call ResourceRef.reload() but not when the source signals change | | Resolved | Loading/Reloading has completed and the resource has the value returned from the loader. | | Local | The resource's value was set locally via .set() or .update(). **Important**: this is different from Resolved, so you probably should use it in conjunction if you plan to check for loaded status *and also* modifying the resource manually | ### `ResourceRef` simplified API | Property | Description | Inherited From | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | | value | A WritableSignal holding the current value of the resource, or undefined if there is no current value (for instance if the call has not been made yet or it is still loading). *Caution*: using set or update on this will set the status signal to ResourceStatus.Local | WritableResource | | status | A Signal indicating the current status of the resource (e.g., 'Loading', 'Error', 'Resolved'). | Resource | | error | A Signal holding the last known error from the resource, if in the Error state. | Resource | | isLoading | A Signal indicating whether the resource is currently loading a new value or reloading the existing one. Is true when the status is Loading or Reloading, making it useful for loader checks | Resource | | hasValue() | A reactive function that returns true if the resource has a valid current value. | WritableResource | | reload() | Instructs the resource to reload its asynchronous dependency. Returns true if a reload was initiated, false otherwise. | Resource | | set(value) | Convenience method for setting the value of the resource. Use this instead of ResourceRef.value.set | WritableResource | | update(updater) | Convenience method for updating the value of the resource using an updater function. Use this instead of ResourceRef.value.update | WritableResource | | asReadonly() | Returns a readonly version of this resource. | WritableResource | | destroy() | Manually destroys the resource, canceling pending requests and returning it to the idle state. Use this to cancel HTTP calls manually | ResourceRef | ## In Conclusion `resource`/`rxResource` are a great addition to the Angular ecosystem, and they are definitely expanding on the signals story that has been evolving for more than a year now. `resource` is also the first step towards making Angular apps not dependant on `HttpClient`, which is one of the things that make Angular apps still dependant on RxJS. As Angular team moves in the direction of making RxJS optional, it is important to have tools in place that help replace existing tools that depend on RxJS, and `resource` is one big step towards that. Finally, `resource`/`rxResource` are great tools to promote declarative programming in Angular apps, and as we all know too well, HTTP requests, especially ones that depend on dynamic values, have been a big source of imperative programming. ## Small promotion ![Modern Angular.jpeg](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Modern-Angular.jpeg) The recent upheaval in Angular has caused many developers to be confused about what solutions to chose, how to implement them, and how to migrate their existing codebases to the most recent features. Thankfully, I have a response to this concern: very soon, my very first book is going into print! It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular?ref=angularspace.com). The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on [Twitter](https://twitter.com/Armandotrue?ref=angularspace.com) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/?ref=angularspace.com), where I will be posting whenever there are news or promotions available. P.S. Hey! Check out chapter 5 of my book to learn more about RxJS interoperability and chapter 7 to dive deep into signals and how they work under the hood ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/Screenshot-2024-11-20-at-17.03.00.png) - [Matthieu Riegler ](https://x.com/Jean%5F%5FMeche?ref=angularspace.com) - [Alain Boudard](https://x.com/aboudard?ref=angularspace.com) ### Creating Custom rxResource API With Observables URL: https://www.angularspace.com/creating-custom-rxresource-api-with-observables/ Last updated: 2024-11-14T15:19:48.000Z At the time of writing this article, Angular is approaching the version 19 release and it [brings a new API called resource](https://github.com/angular/angular/pull/58255?ref=angularspace.com). A great in-depth article is to read [Enea Jahollari - Everything you need to know about the resource API](https://push-based.io/article/everything-you-need-to-know-about-the-resource-api?ref=angularspace.com). While the resource API is available from version 19, [Angular npm downloads](https://www.npmjs.com/package/@angular/core?activeTab=versions&ref=angularspace.com) show that many projects are still stuck on version 12-15. Since these older versions heavily use Observables, I want to attempt creating a similar wrapper as `rxResource`, only it would work with Observables instead of signals. ## Application Overview Below you can see a simple app on which we can demonstrate the main functionalities of `rxResource` API. Our goals is to fulfill the following requirements: - As the user increments the counter, load more items - Display the loading state while the data is being loaded - Display the error state if the HTTP call fails - Create a refresh button that will reload the data - Enable editing displayed items - remove them from the UI by clicking ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/resource-example-1.gif) Application Example That We Will Be Building ```TS @Component({ selector: 'app-resource-normal-example', standalone: true, imports: [FormsModule], template: `

Resource Normal Example

@if (todosResource.isLoading()) {
Loading...
} @else if (todosResource.error()) {
{{ todosResource.error() }}
} @else if (todosResource.hasValue()) { @for (item of todosResource.value() ?? []; track $index) {
{{ item.id }} -{{ item.title }}
} }
`, }) export class ResourceNormalExample { private http = inject(HttpClient); limitControl = signal(5); todosResource = rxResource({ request: this.limitControl, loader: ({ request: limit }) => { return this.http.get( `https://jsonplaceholder.typicode.com/todos?_limit=${limit}` ).pipe( map((res) => { if (limit === 8) { throw new Error('Error happened on the server'); } return res; }), delay(1000), ); }, }); onRemove(todo: Todo) { this.todosResource.update( (d) => d?.filter((item) => item.id !== todo.id) ); } } ``` To briefly summarize, whenever the `limitControl` signal changes, it will rerun the `loader` part of the `rxResource`. The `loader` takes the limit value as an argument and creates an HTTP request. We use `delay` to simulate slow network (to display the loading state) and if you select number 8, an error will be thrown to demonstrate the error state. On every click on a single displayed Todo item, the item will be removed from the `todosResource`, and, lastly, there is a refresh button to reload todos from the API. ## Creating a Custom rxResource This part will be divided into two section. First, we create a basic `rxResourceCustomBasic` function that will return the state of the request (loading, loaded, etc.) with the loaded data, and then we will create a more complex version of this to also allow refreshing, updating and setting a new value manually. ## Defining The Correct Types We need to define what types we are working with, such as the data, the error and the loading state of the request, so we may go with something like the following: ```TS // this is the primary type we will be working with type RxResourceCustomResult = { /** * states: * - `loading` - the resource is loading * - `loaded` - the resource has been loaded * - `error` - an error occurred while loading the resource * - `local` - the resource has been set/modified locally */ state: 'loading' | 'loaded' | 'error' | 'local'; isLoading: boolean; data: T | null; error?: unknown; }; // in theory you could also go with the one below, but // its usage were a bit different from what Angular's rxResource has type RxResourceCustomResult = { state: 'loading' } | { state: 'local' } | { state: 'loaded', data: T | null; } | { state: 'error', error: unknown; } ``` ### Creating A Function Skeleton To create the skeleton for the custom function that will look the same as Angular’s rxResource, we may go with something like: ```TS export const rxResourceCustomBasic = (data: { request: any[]; loader: (values: any) => Observable; }): Observable> => { // todo .... return of({} as RxResourceCustomResult); } ``` The `request` accepts an array of something, ideally observables. It will differ from `rxResource` in syntax that we are expecting an array, however `rxResource` uses the following syntax `request: () => ({limit: this.limitControl})`. The `loader` is a closure function provided by the user, that can access (the observable) values from the `request` parameter and it returns an observable (a HTTP request). Since initially everything is a type of `any`, TS doesn’t suggest that the `value` inside the `loader` array should a be number. Let’s fix it: ```TS // extract the value from an observable type ObservableValue = T extends Observable ? U : never; export const rxResourceCustomBasic = < T, TLoader extends Observable[] >(data: { request: [...TLoader]; loader: (values: { [K in keyof TLoader]: ObservableValue; }) => Observable; }): Observable> => { return of({ } as RxResourceCustomResult); } ``` A bit of generics are used, but what’s happening is that the first `T` in `rxResourceCustomBasic` represents the shape of the data from an API and `TLoader extends Observable[]` represents an array of observable dependencies that we will be listening to. I will not explain the whole generic structure, you can visit the [Github example](https://github.com/krivanek06/stackblitz-angular-custom-resource-function?ref=angularspace.com) if you want to investigate what happens by tweaking the types. Now when you use the `rxResourceCustomBasic` you will see the correct types. For example the `loader` has exactly 3 parameters and each has its own correct type. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/i2.png) Custom rxResource wrapper with correct types ## Basic Custom rxResource Types are correctly solved, now let’s create the basic version of our custom rxResource, that has the state, data and error properties. ```TS export const rxResourceCustomBasic = < T, TLoader extends Observable[] >(data: { request: [...TLoader]; loader: (values: { [K in keyof TLoader]: ObservableValue; }) => Observable; }): Observable> => { // listen to all the requests observables return combineLatest(data.request).pipe( switchMap((values) => // execute the loader function provided by the user data .loader( values as { [K in keyof TLoader]: ObservableValue; }, ) .pipe( switchMap((result) => of({ state: 'loaded' as const, data: result, }), ), // setup loading state startWith({ state: 'loading' as const, data: null, }), // handle error state catchError((error) => of({ state: 'error' as const, error, data: null, }), ), // map the result to the expected type map( (result) => ({ ...result, isLoading: result.state === 'loading', }) satisfies RxResourceCustomResult, ), ), ), // share the observable shareReplay(1), ); }; ``` - We use `combineLatest` to listen on any new emission from the array of observables in `data.request` and we want to rerun the logic if any of those observables emit a new value. - I use casting `value as ....` because TS suggest that `value` is a type of `unknown[]`. Not sure how to fix it, this was the easiest way. - The `data.loader(values)` is used to provide the arguments from the `request` part into the `loader` part function provided by the user. - Using the rxjs `startWith`, the initial state is `loading` and every time any observable emits, the state will always start with the `loading` state. - Using the `catchError` to handle error state and add it to the source that can throw an error. If you are confused why `catchError` is not the last operator, consider checking out [Angular Rxjs - CatchError Position Matter!](https://dev.to/krivanek06/angular-rxjs-catcherror-position-matter-3b00?ref=angularspace.com) - The `shareReplay()` operator is used to broadcast a newly emitted value to each subscriber, making it a hot observable ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/i3.png) Using the basic custom rxResource ## Advanced Custom rxResouce Although, the basic example works fine, there are some small issues with it. The main one is that the `rxResourceCustomBasic` returns an observable and we always have to subscribe on it, even if we only want the most current value. Second, we are missing some methods, which the Angular’s `rxResource` provides, such as `reload()`, `update()` and `set()`. Let’s create a more advanced version that will have all the above mentioned features. ```TS export type RxResourceCustom = { /** * Trigger a reload of the resource */ reload: () => void; /** * @returns the current result of the resource */ value: () => T | null; /** * @param updateFn - function to update the current data */ update: (updateFn: (current: T) => T) => void; /** * @param data - set the data of the resource */ set: (data: T) => void; /** * Observable of the resource state */ result$: Observable>; }; export const rxResourceCustom = < T, TLoader extends Observable[] >(data: { request: [...TLoader]; loader: (values: { [K in keyof TLoader]: ObservableValue; }) => Observable; }): RxResourceCustom => { // Subject to trigger reloads const reloadTrigger$ = new Subject(); // hold the latest result of type `T | null` const resultState$ = new BehaviorSubject<{ state: RxResourceCustomResult['state']; data: T | null; }>({ state: 'loading' as const, data: null, }); const result$ = reloadTrigger$.pipe( startWith(null), // listen to all the requests observables combineLatestWith(...data.request), // prevent request cancellation exhaustMap(([_, ...values]) => // execute the loader function provided by the user data .loader( values as { [K in keyof TLoader]: ObservableValue; }, ) .pipe( switchMap((result) => of({ state: 'loaded' as const, data: result, }), ), // setup loading state startWith({ state: 'loading' as const, data: null, }), // handle error state catchError((error) => of({ state: 'error' as const, error, data: null, }), ), ), ), ); // subscribe to the result and update the state result$.pipe(takeUntilDestroyed()).subscribe( (state) => resultState$.next(state) ); return { result$: resultState$.asObservable().pipe( map((state) => ({ ...state, isLoading: state.state === 'loading', })), ), reload: () => reloadTrigger$.next(), value: () => resultState$.value.data, update: (updateFn: (current: T) => T) => { const current = resultState$.value; if (current?.data) { resultState$.next({ state: 'local', data: updateFn(current.data), }); } }, set: (data) => { resultState$.next({ state: 'local', data: data, }); }, }; }; ``` When user executes the `reload()` function, it will next the `reloadTrigger$` and re-execute the provided logic inside the `request` parameter. Using `exhaustMap` allows us to ignore subsequent requests when the user triggers the `reload()` function multiple times until the first one is not finished. With the `combineLatestWith` operator we are constantly listening on the observable dependencies (from `request`) and if any of them emit, the logic (inside the `loader`) is re-executed. The`resultState$` behaviourSubject is used to keep the current state of the `loader` section, so that we can return the current value and modify the cached result. You may see that in this example we replaced the `shareReplay(1)` with `takeUntilDestroyed()`, meaning, whenever you leave the execution context (component is destroyed), the subscription is cancelled and you avoid memory leaks. **Note**: Keep in mind that `takeUntilDestroyed` operator is available from [Angular version 16](https://blog.angular.dev/angular-v16-is-here-4d7a28ec680d?ref=angularspace.com), so you need to be on at latest version 16 when using the above advanced example. ## Using The Custom rxResource Finally we can now use the custom `rxResourceCustom` that we’ve created as follows: ```TS @Component({ selector: 'app-resource-custom-example', standalone: true, imports: [ReactiveFormsModule, AsyncPipe], template: `

Resource Custom Example

@if (todosResource.result$ | async; as data) { @if (data.isLoading) {
Loading...
} @else if (data.error) {
{{ data.error }}
} @for (item of data.data; track $index) {
{{ item.id }} - {{ item.title }}
} }
`, styles: [], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ResourceCustomExampleComponent { private http = inject(HttpClient); limitControl = new FormControl(5, { nonNullable: true }); private limitValue$ = this.limitControl.valueChanges.pipe( startWith(this.limitControl.value) ); todosResource = rxResourceCustom({ request: [this.limitValue$], loader: ([limit]) => { return this.http.get( `https://jsonplaceholder.typicode.com/todos?_limit=${limit}` ).pipe( map((res) => { if (limit === 8) { throw new Error('Error happened on the server'); } return res; }), delay(1000), ); }, }); onRemove(todo: Todo) { this.todosResource.update( (d) => d?.filter((item) => item.id !== todo.id) ); } constructor(){ // log the current value console.log(this.todosResource.value()); } } ``` ## The Final Result The final result of the side by side comparison of the Angular’s rxResource on the left, and our custom observable rxResource on the right ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/result-final.gif) The Final Result Of rxResource Compared To rxResouceCustom Hope you liked the article and found it helpful. If you want to play around with it, you can check the [Github repo](https://github.com/krivanek06/stackblitz-angular-custom-resource-function?ref=angularspace.com). Also feel free to connect with me on [LinkedIn](https://www.linkedin.com/in/eduard-krivanek?ref=angularspace.com) or visit my [dev.to](https://dev.to/krivanek06?ref=angularspace.com) account for more content. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/Screenshot-2024-11-14-at-16.15.08.png) - [Alain Boudard](https://x.com/aboudard?ref=angularspace.com) - [Armen Vardanyan](https://x.com/Armandotrue?ref=angularspace.com) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/angular-university-banner-4--1-.jpg)](https://angular-university.io/?ref=angularspace.com) ### Magic with Interceptors URL: https://www.angularspace.com/magic-with-interceptors/ Last updated: 2024-11-14T15:20:26.000Z # Introduction [Angular interceptors](https://angular.dev/guide/http/interceptors?ref=angularspace.com) are a feature that is widely known, but seldom used aside from the most basic needs (authentication, authorization). However, there are lots of use cases when interceptors can be very useful. In this article, we will explore some interesting and very common scenarios in which interceptors can come to our aid. ## What is an interceptor? Well, first, let us understand what an interceptor is. An Angular interceptor is (in a modern setting anyway) a function that utilizes the ["Chain of Responsibility"](https://refactoring.guru/design-patterns/chain-of-responsibility?ref=angularspace.com) design pattern and allows us to interject logic into the HTTP request/response cycle. This means we can "catch" the request, modify it, and then pass it on to the next interceptor in the chain and so on. Then we can do the same with the response (this will come handy later). Now, with this knowledge, lets embark on a journey of creating several interceptors to make our code that deals with HTTP much simpler. ## The most basic example Let's get this one out of the way quickly: almost everyone has written an `AuthInterceptor` in their app. Here is a simplistic example: ```typescript export const authInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const token = authService.getToken(); if (token) { req = req.clone({ setHeaders: { Authorization: `Bearer ${token}`, } }); } return next(req); }; ``` Here we just pick the token from the `AuthService` and add it to the request headers. Pretty simple, right? However, we should be careful with this. In [his article](https://timdeschryver.dev/blog/watch-out-what-you-expose-with-angular-interceptors?ref=angularspace.com), Tim Deschryver explores a security issue that arises from this approach: what if we send requests to other, 3rd-party APIs too? This way, our request will be exposed to that API, and they might be able to read sensitive data about our users. We can fix this by checking the URL before adding the token: ```typescript export const authInterceptor: HttpInterceptorFn = (req, next) => { const uri = new URL(req.url); if (uri.hostname !== 'trusted-domain.com') { return next(req); } const authService = inject(AuthService); const token = authService.getToken(); if (token) { req = req.clone({ setHeaders: { Authorization: `Bearer ${token}`, } }); } return next(req); }; ``` Great, we figured our authentication out. So, what else can we do? ## Altering the request URL In large applications, it is common to have several environments, and several API URLs, from which we then would have to pick one to use. It is customary to store such information in an environment file (this might differ from codebase to codebase), so we will assume we have an `Environment` injectable that provides us with the current environment and its API URL. However, this does not solve the problem of having to constantly include the URL when performing the request: ```typescript @Injectable({providedIn: 'root'}) export class DataService { private readonly http = inject(HttpClient); private readonly environment = inject(Environment); getData() { return this.http.get(this.environment.apiUrl + '/data'); } getDataById(id: number) { return this.http.get(this.environment.apiUrl + `/data/${id}`); } } ``` As we can see, we have to include the URL in the request every time we want to make a request. While this is not a big deal, this might be a source of short, but frustrating bugs, and, additionally, become a big headache if the logic of determining an API URL changes (might have us refactoring a billion API call methods). So, we can just use an interceptor for this: ```typescript export const apiUrlInterceptor: HttpInterceptorFn = (req, next) => { const environment = inject(Environment); const baseUrl = environment.getAPIUrl(); req = req.clone({ url: `${baseUrl}/${req.url}`, }); return next(req); }; ``` Now, our service can simply do the HTTP call with a relative URL: ```typescript @Injectable({providedIn: 'root'}) export class DataService { private readonly http = inject(HttpClient); getData() { return this.http.get('/data'); } getDataById(id: number) { return this.http.get(`/data/${id}`); } } ``` Next, let us consider the connection of interceptors to the lifecycle and the state of our application. ## Interacting with application state from interceptors Consider the following example: we want to display a small loading bar on top of any page when an HTTP request is in progress. To further demonstrate the capabilities of interceptors, we will also assume that our application is using some state management solution like NgRx, and we have an action called `setLoading` that sets the loading state to `true` or `false`. This way, because we can use DI in interceptors, we can use our interceptor to affect the UI of our application and display that loading bar: ```typescript export const loaderInterceptor: HttpInterceptorFn = (req, next) => { const store = inject(Store); store.dispatch(setLoading(true)); return next(req).pipe( tap((res) => { if (res.type === HttpEventType.Response) { store.dispatch(setLoading(false)); } }) ); }; ``` As you can see, in this interceptor, we actually interact more with the response than the request. This will become a recurring pattern in further interceptors, as we will see. With this interceptor, our methods don't have to change slightly, and the component will just get it's data from the store, without ever knowing how the `loading` got to be `true` or `false`. But what if don't want to show the loading bar on every single request? For instance, if we are doing some "undercover" HTTP requests like logging, or third party initializations, we might want to skip showing loaders for those as to not create an impression of having a very heavy application. So how we do this? ## Adding context to interceptors. Essentially, Angular provides us with a way to add custom context to HTTP requests. This is accomplished via the `HttpRequestContextToken` token. This is a class that allows us to define some custom metadata to pass on with a particular request, which then can be read and acted upon by interceptors. Let's see how we can use this to our advantage: ```typescript export const NoLoaderToken = new HttpContextToken(() => false); @Injectable({providedIn: 'root'}) export class LoggerService { log(data: any) { return this.http.post('log', {context: NoLoaderToken, body: data}); } } ``` Here, we use the `HttpContextToken` to define a `NoLoaderToken` token that we can use to skip the loader. Here is how modify our interceptor to use this token: ```typescript export const loaderInterceptor: HttpInterceptorFn = (req, next) => { if (req.context.get(NoLoaderToken)) { return next(req); } const store = inject(Store); store.dispatch(setLoading(true)); return next(req).pipe( tap((res) => { if (res.type === HttpEventType.Response) { store.dispatch(setLoading(false)); } }) ); } }; ``` Now, we can clearly differentiate between requests that should not have a loader and those that should! Next, let us discuss resolving tensions between front-end and back-end using interceptors. ## Throwing on implicit errors In some APIs (quite commonplace nowadays), instead of having some sort of HTTP error code, we might have a response that is just an object with a boolean indicating if the request was successful or not. The typical response from such an API might look like this: ```json { "success": true, "data": { "id": 1, "name": "John Doe" }, "error": null } ``` Whether this is a good idea or not is still up for a debate, however, this can become a problem when front-end and back-end developers have different opinions on the whole thing. It can also be problematic when handling errors in services, as we might want to double-check: ```typescript @Injectable({providedIn: 'root'}) export class DataService { private readonly http = inject(HttpClient); getData() { return this.http.get('/data').pipe( map(res => { if (res.success) { return res.data; } // handle error in some way }), catchError((err) => { // notice that even with this approach, // we still have to write `catchError`, // as errors can arise not only from the backend // but also from network connection // bugs in our won code and so on }) ); } } ``` So, how do we address this in a way that helps us avoid duplicating the same code all over our project files? Surely, intercepting responses will give us an answer: ```typescript export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( map(res => { if (res.type === HttpEventType.Response) { const body = res.body as {success: boolean, message?: string}; if (body.success === false) { throw new HttpErrorResponse({error: body.message}); } return res; } return res; }), ); } ``` Here, we interject a piece of logic that detects if the backend responded with `{success: false}` and throws an actual error. Now, our service can just use the `catchError` operator to handle the error, no need to double-check. ```typescript @Injectable({providedIn: 'root'}) export class DataService { private readonly http = inject(HttpClient); getData() { return this.http.get('/data').pipe( map(res => res.data), catchError((err) => { // handle error in some way }) ); } } ``` Next, let us see how we can modify the body itself. ## Altering the body We will continue looking at the previous example, and notice that because of this type of backend response, we constantly use `map(res => res.data)` to get the data itself from the response. Again, we can just use an interceptor to alter the body before it ever gets to the service: ```typescript export const responseUnwrapInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( map(res => { if (res.type === HttpEventType.Response) { const body = res.body as {success: boolean, data?: any, message?: string}; if (body.success === true) { return res.clone({body: body.data}); } return res; } return res; }), ); } ``` And that's it, now our components (or NgRx Effects, for instance), can just read the data, or deal with an error using `catchError` without having to worry about the form of the response from backend. ## Final things to consider As we saw, interceptors are a powerful tool, but as anything that affects the *entire* application, it is important to be careful. We already saw a way to introduce a security vulnerability in the very first example, and we also saw potential issues where we got help from the `HttpContextToken` class. Another thing that is important to keep in mind is that interceptors are executed in the same order as they are provided in our application configuration, so it is important to order them in a logical way. For instance, our `authInterceptor` uses the hostname for security reasons, so we need to place it *after* the `apiUrlInterceptor` in order to have it work. Then we can use the `loaderInterceptor`. Finally, response interceptors should probably come after the ones that handle only the request part, and in our case, the `errorInterceptor` should come *before* the `responseUnwrapInterceptor`, as we want to catch errors before we try to unwrap the response. Ideally our config will look like this: ```typescript export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([ authInterceptor, apiUrlInterceptor, loaderInterceptor, errorInterceptor, responseUnwrapInterceptor, ]) ), ], }; ``` So, it is important to keep all these concerns in mind. ## Conclusion Interceptors can be amazing, and are, again, underutilized, while being able to significantly simplify our code in regards to simple, recurring tasks. There are many, many more concerns that we did not cover here, like caching, logging, and so on, but some of them are already mentioned in the official docs, and can be easily implemented. Hopefully, this helps readers grasp way more capabilities that interceptors provide, and improve their codebases. ## Small promotion ![Modern Angular.jpeg](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Modern-Angular.jpeg) You might have noticed that this article is using function-based interceptors only. The recent upheaval in Angular has caused many developers to be confused about what solutions to chose, how to implement them, and how to migrate their existing codebases to the most recent features. Thankfully, I have a response to this concern: very soon, my very first book is going into print! It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular?ref=angularspace.com). The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on [Twitter](https://twitter.com/Armandotrue?ref=angularspace.com) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/?ref=angularspace.com), where I will be posting whenever there are news or promotions available. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/Screenshot-2024-09-17-at-15.24.51--2--1.jpg) - [Matthieu Riegler ](https://x.com/Jean%5F%5FMeche?ref=angularspace.com) - [Alain Boudard](https://x.com/aboudard?ref=angularspace.com) ### Unique Angular Style Guide by Angular Space URL: https://www.angularspace.com/unique-angular-style-guide-by-angular-space/ Last updated: 2024-11-01T10:51:23.000Z I'm happy to announce that we are starting the work on opinionated and unique Angular Style Guide and Best Practices resource. First I asked community members what do they think, and response was amazing! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/11/GbPP1OrakAQGi8G_upscayl_2x_ultrasharp.png) It's going to be built collaboratively by Angular Space & Angular Space Mentors, taking community feedback on each rule/practice. Like with our articles, quality is the main goal. Why unique? - Goal is to help teams make thoughtful, adaptable style choices for Angular projects. - We are going to provide strong recommendations for those who just want the answer. - We are going to provide context on recommendations fit best and when alternatives may work better for those seeking to adapt it to their own needs. - A complete sheet of clear info with our thought process that will help you make the educated decisions or just take the recommendation and go with it 😃 - Something for lazy and something for more demanding. - It's going to be regularly aligned with official Angular updates. Let’s build a guide that grows with the Angular community! You can find aa GitHub repo here: [GitHub - Angular-Space/angular-style-guide: Opinionated Angular Style Guide + Best Coding Practices. With collaboration of Angular Space & Angular Space Mentors! Angular Space portal has been created for the community and this style guide is going to follow the same mindset. Same as our articles, we want to keep it high quality but we will also take community suggestions :) (WIP)Opinionated Angular Style Guide + Best Coding Practices. With collaboration of Angular Space & Angular Space Mentors! Angular Space portal has been created for the community and this style guid…![](https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg)GitHubAngular-Space![](https://opengraph.githubassets.com/e85eeb5dc050b7ead6a8e07db4fe15df382cca4472e2c7f29aa02f3e6cef9f78/Angular-Space/angular-style-guide)](https://github.com/Angular-Space/angular-style-guide?ref=angularspace.com) ### Directive Best Practices URL: https://www.angularspace.com/directive-best-practices/ Last updated: 2024-10-28T15:32:52.000Z There are many articles online about Angular best practices, or even best practices for components specifically. Of course, components are the most important building blocks of the framework, but we know that directives are almost as important, and when it comes to them, there are also certain patterns of code that are preferable to other. So, today, let's take a look at directive best practices and figure out how to write the cleanest possible code to enrich our templates. ## Alias inputs that match directive selectors A popular pattern with directives that comes up pretty often is a directive that has exactly one input and does a specific thing. Very often developers will give that input the same name as the directive's attribute selector to simplify the template where it can be used. Now, let's take a look at this simple directive that displays a tooltip: ```typescript @Directive({ selector: '[appTooltip]', }) export class TooltipDirective implements AfterViewInit { // input for custom text appTooltip = input.required(); ngAfterViewInit() { // code that adds the tooltip } } ``` This naming allows us to use the tooltip in a very simple way in a template: ```html
Content
``` However, the directive code suffers a bit, as `appTooltip` is not a very descriptive name, which can confuse someone who is trying to understand the code. To keep both the template and the directive code clean, we can instead alias the input to the attribute selector: ```typescript @Directive({ selector: '[appTooltip]', }) export class TooltipDirective implements AfterViewInit { // aliased input for custom text tooltipText = input.required({alias: 'appTooltip'}); ngAfterViewInit() { // code that adds the tooltip } } ``` This way, we can still use the directive in the template the same way as before, but the directive code is now somewhat improved. ## Utilize more than just attribute selectors As we already touched directive selectors, we can talk a bit more about them. An (anti)pattern that is very common among Angular developers is making up a custom attribute name for every directive. However, as we shall see, this is not always necessary. Consider this directive, that, when applied to an input, validates an email address: ```typescript @Directive({ selector: '[appEmailValidator]', }) export class EmailDirective implements Validator { validate(control: AbstractControl) { // validate with some custom regex return control.value.match(/.+@.+\..+/) ? null : { email: true, }; } } ``` Then we can apply it whenever we use an email input: ```html ``` However, directive selectors are way more versatile than just attribute selectors, and there is no need to constantly put the `appEmailValidator` attribute on inputs, because we can simply target all inputs of type "email": ```typescript @Directive({ selector: 'input[type=email]', }) export class EmailDirective implements Validator {...} ``` Then, any time we create an input of type "email", the validator will get automatically applied (given that we imported the directive into the component). ## Use `host` to apply dynamic styles Another common use case for directives is applying some custom styles to the host element depending on some conditions. Often, developers do it "manually". Let's take a look at this directive which checks blocks the UI for an element if the user has no certain permission: ```typescript @Directive({ selector: '[appBlockUI]', }) export class BlockUIDirective implements OnInit { private readonly permissionsService = inject(PermissionService); private readonly elementRef = inject(ElementRef); permissionName = input.required({alias: 'appBlockUI'}); ngAfterViewInit() { this.permissionsService.getPermissions() .subscribe(permissions => { if (!permissions.has(this.permissionName())) { this.elementRef.nativeElement.style.opacity = '0.5'; this.elementRef.nativeElement.style.pointerEvents = 'none'; } else { this.elementRef.nativeElement.style.opacity = '1'; this.elementRef.nativeElement.style.pointerEvents = 'auto'; } }); } } ``` Here, while the directive "does the job", the code inside it reads as very "imperative", we give commands and accessing the DOM directly. That's not what we usually do in Angular, and in this case, it is full avoidable. We can instead utilize signals and RxJS interop, and rely on the `host` metadata property to bind the results from the service to the DOM element: ```typescript @Directive({ selector: '[appBlockUI]', host: { '[style.pointerEvents]': 'styles().pointerEvents', '[style.opacity]': 'styles().opacity', }, }) export class BlockUIDirective { private readonly permissionsService = inject(PermissionService); permissionName = input.required({alias: 'appBlockUI'}); permissions = toSignal(this.permissionsService.getPermissions()); styles = computed(() => { const hasPermission = this.permissions().has(this.permissionName()); return ({ pointerEvents: hasPermission ? 'auto' : 'none', opacity: hasPermission ? 1 : 0.5, }); }); } ``` As we can see, this becomes a lot more readable and maintainable. In general, directives have the potential to be even more declarative in code style than components, making it super important to keep them clean and easy to understand. ## Do not use boolean inputs to apply the directive conditionally Sometimes, we might write a selector that neatly matches all of the elements we want, but also catches some scenarios where we don't really want the directive to be applied. Consider this example: ```typescript @Directive({ selector: '[routerLink]', host: { '(mouseover)': 'showPreview()', '(mouseout)': 'hidePreview()', }, }) export class PreviewLinkDirective { routerLink = input.required(); showPreview() { // some logic that generates a preview on hover } hidePreview() { // some logic that hides the preview on mouseout } } ``` Here, we have a directive that, when applied to an element, shows a preview of the link when the user hovers over it, and hides it when the user moves the mouse away. However, sometimes we might just not want to show the preview, for instance, for some internal links, like a login page, the preview just doesn't make sense. We could do something like this: ```typescript @Directive({ selector: '[routerLink]', host: { '(mouseover)': 'showPreview()', '(mouseout)': 'hidePreview()', }, }) export class PreviewLinkDirective { routerLink = input.required(); showPreview = input(true); showPreview() { if (this.showPreview()) { // some logic that generates a preview on hover } } hidePreview() { if (this.showPreview()) { // some logic that hides the preview on mouseout } } } ``` This way, we can pass a false value to the directive to disable the preview, and the logic will not be applied. This approach, sadly, has two downsides 1. The directive becomes (as we will see) unnecessarily verbose. 2. The directive still gets applied, so if a future developer adds a new `host` listener and forgets to use the `showPreview` input, a bug will be possibly introduced. So, how can we address this? Well, we can use the `:not()` CSS selector, which directive selectors support, to avoid applying the directive *at all* in certain cases. Here's how we can do it: ```typescript @Directive({ selector: '[routerLink]:not([noPreview])', host: { '(mouseover)': 'showPreview()', '(mouseout)': 'hidePreview()', }, }) export class PreviewLinkDirective { routerLink = input.required(); showPreview() { // some logic that generates a preview on hover } hidePreview() { // some logic that hides the preview on mouseout } } ``` Then, in the template, whenever we want a certain link to not have a preview, we can just add that attribute: ```html Some link ``` Now, the directive will not be applied to that link at all, and there is no need to introduce conditional logic to the directive code. ### Important to know While in 90% cases the approach we just described is the best practice, it is not completely infallible, and sometimes boolean inputs enabling/disabling the directive functionality are still necessary. To understand why, we need to first understand how Angular directives *actually* work. Hint: it is a bit more complex than you might think. From outside, it might seem that when we write a selector, the DOM is being searched for matched elements, and, when found, the directive is applied to them. However, this is not the case *at all*. In reality Angular directives are applied to DOM elements at *compile-time*, not during run-time. This means that when we run `ng serve` or `ng build`, and Angular actually builds our application and compiles our templates to executable JavaScript commands, it takes into consideration the directives that a given component imports, and, if a corresponding element is found, the directive is applied to it. While this might seem like not that big of a deal, it has massive implications. First and foremost, it means we cannot apply directives dynamically. Even if we use `Renderer2` to add an attribute to an element that perfectly matches with some directive, there is no mechanism in Angular that will see that change and apply the directive to the element at runtime. This also means that when we used the negation selector (`:not(noPreview)` in the previous example), we excluded that certain element from having that directive *forever*. There is no way to dynamically apply it back. In most of scenarios where this approach is used, we really want to exclude an element for good; maybe the directive selector is fine, but too broad, and some particular elements don't need that, so we use selector negation. However, if the execution of the directive code is dependant on an external condition, and we want the logic re-executed when that condition changes, we are completely justified in using a boolean input. ## Try to avoid using directives for custom events Sometimes, we want to create a directive that will trigger a custom event on the host element. For example, we might be building a UX that utilizes keyboard shortcuts like `Ctrl + Click` or `Ctrl + Enter` a lot. We might be tempted to write a directive that will listen to those events and trigger a custom event on the host element, like this: ```typescript @Directive({ selector: '[appCtrlClick]', host: { '(click)': 'handleClick($event)', }, }) export class CtrlClickDirective { appCtrlClick = output(); handleClick(event: MouseEvent) { if (event.ctrlKey) { this.appCtrlClick.emit(event); } } } ``` We can then use it in a template somewhere: ```html ``` Of course, it works pretty well, but has a few downsides: 1. The directive is not very generic, for other events, we would need to either create another directive, or complicate this one. 2. We need to import it everywhere we use. 3. Angular has a conventional way of dealing with custom events, and we can utilize it. Instead of using the directive here, we can use the [EventManagerPlugin](https://angular.dev/api/platform-browser/EventManagerPlugin?ref=angularspace.com) and define a custom plugin for `Ctrl + Click` events: ```typescript export class CtrClickPlugin extends EventManagerPlugin { override supports(eventName: string): boolean { // catch all ctrl.click events return eventName === 'ctrl.click'; } override addEventListener( element: HTMLElement, eventName: string, originalHandler: EventListener ) { // wrap the original handler with Ctrl check const handler = (event: MouseEvent) => { if (event.ctrlKey) { originalHandler(event); } }; element.addEventListener('click', handler); return () => { // remove the listener element.removeEventListener('click', handler); }; } } ``` Then, we have to provide the plugin in the application configuration: ```typescript export const appConfig: ApplicationConfig = { providers: [ { provide: EVENT_MANAGER_PLUGINS, useClass: CtrClickPlugin, multi: true, }, ], } ``` Then, we can freely use the event in any component template we wish: ```html ``` This makes it fully reusable, and easier to customize (we can add multiple event plugins, or different functions to choose from depending on the event name to handle `Ctrl + Enter`, `Ctrl + Right Click` and so on). ## Conclusion Directives are a powerful and versatile tool, and as with any other tool, they can be easily abused. However, with the ruleset that this article provides, we hope the readers will enjoy righting simpler, less error-prone and easier-to-understand directives to utilize their full power. ## Small promotion As you see, in most examples discussed, we use signals, signal inputs/outputs, and so on, all the things that are quite new to Angular. The recent upheaval in Angular has caused many developers to be confused about what solutions to chose, how to implement them, and how to migrate their existing codebases to the most recent features. Thankfully, I have a response to this concern: very soon, my very first book is going into print! It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular?ref=angularspace.com). The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on [Twitter](https://twitter.com/Armandotrue?ref=angularspace.com) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/?ref=angularspace.com), where I will be posting whenever there are news or promotions available. P.S. Hey! Check out chapter 4 of my book if you're curious Angular directives so you can learn about the power of `HostDirective`\-s, and chapters 6-7 to dive deep into signals ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/Screenshot-2024-08-28-at-09.39.59-1-1.jpg) ### Exclusive Angular Discord Community Invite URL: https://www.angularspace.com/exclusive-angular-discord-community-invite-copy/ Last updated: 2024-11-01T13:42:52.000Z Hey! Angular Space just gained around 300 Members in a span of 1 month! Amazing growth. I'm sharing the Discord Community invite again so everyone interested can join. _This post is for subscribers only._ ### Introduction to Vitest and Angular URL: https://www.angularspace.com/introduction-to-vitest-and-angular/ Last updated: 2024-10-24T13:32:03.000Z The Angular team deprecated Karma a few versions ago and are currently working on ways to provide an alternative 3rd party unit testing frameworks. Currently the options talked about so far are Web Test Runner (likely to be the default), this is a browser based unit test runner similar in many ways to Karma. The other option being discussed is Jest, this can be installed now but can be a little tricky sometimes to get set up and some known issues when Angular is set up to use ES builds (the default in Angular 18 onward). The Angular team is looking at using the CLI for the installation process of these unit testing frameworks, similar to selecting a CSS framework when creating a new project. This is all still very early on in the development cycle and may be a little way off before it becomes developer preview (currently still experimental) and no mention (as yet) of how this would work with an existing project. Now that newer versions of Angular use Vite as a development build server and the default in Angular 18, it is now possible to use Vitest in our Angular projects. The installation process is straight forward to set up and feels less problematic than setting up Jest from my recent experience. The great news here is that Vitest has a similar syntax to Karma and Jest, so the learning curve should be fairly small. > For more information on the build migration in Angular you can check the documentation at: [Angular build system migration](https://angular.dev/tools/cli/build-system-migration?ref=angularspace.com) In this post I'm going to walk through setting up and configuring Vitest in an Angular 18 application and replacing Karma. I won't be writing unit tests in this post, I'll leave that for another post. I'm going to start a new project for this post but this will also work on an existing project. In a directory type the following in the terminal: ```bash ng new my-vitest-app && cd my-vitest-app && npm i ``` This will create a new Angular application called `my-vitest-app`, it'll change into that directory and then run `npm install` with our application created we can now remove `Karma`. From the terminal, type: ```bash npm uninstall karma karma-chrome-launcher karma-coverage karma-jasmine karma-jasmine-html-reporter ``` We don't need to uninstall `@types/jasmine` and `jasmine-core` as Vitest uses Jasmine under the hood, we can use `it` and `describe` from Jasmine so we don't need to import anything in our test files. It is possible to use `it` and `describe` from Vitest but in doing so we won't be able to use functions like `fakeAysnc` in our unit tests. The easiest way to get Vitest installed in our project is to use a plugin and AnalogJS has just what we need to setup and configure Vitest for us. From the terminal type: ```bash npm i @analogjs/platform -D ``` Followed by: ```bash ng g @analogjs/platform:setup-vitest --project my-vitest-app ``` This package will install the following files: ```json "devDependencies": { // other files removed for brevity "@analogjs/platform": "^1.9.0", "@analogjs/vite-plugin-angular": "^1.9.0", "@analogjs/vitest-angular": "^1.9.0", "@nx/vite": "~19.8.2", "@vitest/coverage-v8": "^2.1.3", "@vitest/ui": "^2.1.3", "vite": "^5.4.9", "vite-tsconfig-paths": "^4.2.0", "vitest": "^2.1.3" } ``` At the time of writing the `@analogjs` library installs `@nx/vite` 19.8.2, Brandon Roberts (AnalogJS creator) has released a beta version that supports `@nx/vite` 20.0.3. This `@analog` package should install 2.1.3 of vitest, @vitest/ui and @vitest/coverage-v8, if it installs 1.6.0 then you will need to uninstall these packages and reinstall them using: ```bash npm i @vitest/coverage-v8 @vitest/ui vitest@latest -D ``` Once complete the following files will be generated: ```typescript // vite.config.mts /// import angular from '@analogjs/vite-plugin-angular'; import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { return { plugins: [ angular(), ], test: { globals: true, environment: 'jsdom', setupFiles: ['src/test-setup.ts'], include: ['**/*.spec.ts'], reporters: ['default'], }, define: { 'import.meta.vitest': mode !== 'production', }, }; }); ``` `vite.config.mts` will be in the root of the project, this sets up the configuration for Vitest, what environment to use, the default is `jsdom`, you can change this to `happy-dom` (you will need to install `happy-dom`). The `test-setup.ts` will be in the `src` directory and this sets up the test environment. ```typescript import '@analogjs/vitest-angular/setup-zone'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing'; import { getTestBed } from '@angular/core/testing'; getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting() ); ``` If you are using a`zoneless` application then the `test-setup.ts` will be slightly different: ```typescript import '@analogjs/vitest-angular/setup-snapshots'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing'; import { getTestBed } from '@angular/core/testing'; getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting() ); ``` In `angular.json` the `test` section will be replaced with: ```json "test": { "builder": "@analogjs/vitest-angular:test" } ``` And finally the `tsconfig.spec.json` will be update to: ```json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", "types": [ "jasmine", "vitest/globals" // added ], "target": "es2016" // added }, "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ], "files": [ "src/test-setup.ts" // added ] } ``` To run Vitest tests we need to change the scripts section, in our applications `package.json` file add: ```json { // other scripts "test" : "vitest" } // we could also use this, if you are not the running vitest command { "test" : "ng test --watch" } ``` Using `npm run test` will run all of our test files our project. If you don't want to install Volta you can skip this section. To run `Vitest` from the terminal we need to install Vitest, we have a couple of options, globally using `npm i -g vitest`, I tend to use Volta, this is a tool chain manager, similar to Node Version Manager (NVM) but Volta handles other JavaScript tooling other than just Node. > If you currently have a version of node installed, you will need to uninstall this first before install Volta, once Volta is installed we can install multiple versions of node without removing the previous versions. To install Volta head over to `https://volta.sh/` and get the appropriate installer package for your operating system, I'm using Ubuntu and all we need for this is: > As a side note, one of the nice features of Volta, is the ability to pin the version of node we are using to our `package.json` file, meaning that Volta will always select the correct version of Node, this is real handy when working in a team of developers, to pin a version of Node to our `package.json` file run `volta pin node@20.15.0`. With `volta` and `node` installed we can now install Vitest: ```bash volta install vitest ``` With Vitest installed we can run `volta ls` this will show a list of the tooling that we have installed. ```bash # example listing from Volta ⚡️ Currently active tools: Node: v20.15.1 (default) Tool binaries available: vitest (current @ /home/repo/vitest-app/package.json) ``` --- With everything installed we can now run our unit tests, if we want to run a selection of tests (or a specific test) then we need to use the terminal and pass in an additional parameter, this parameter will run any test file that contains this parameter. ```bash # run a range of unit tests vitest dashboard ``` We can also specify a file, this will only run a unit test that matches the file name, note that this doesn't support regex or glob patterns. ```bash # run a specific unit test vitest app.component ``` The above will run the unit tests and watch for changes, to run the unit test once and not watch for changes we can change the command to: ```bash # run unit test(s) once then stop vitest run [optionally specify an additional parameter] ``` If we want to run our unit tests in a CI environment we don't need for them to be in watch mode, so add a `test:ci` to the script section of the `package.json` file. ```json { // other scripts "test" : "vitest", "test:ci" : "vitest run" } ``` We need to set up our build pipelines to run our tests, setting up build pipelines is out of scope for this post, so I will assume you know how to do this, if not they are plenty of posts on how to do this. The next part I want to cover is code coverage, so we've written our unit tests for our various features in our project, but have we written enough? Have we covered the important parts that need testing? This is where code coverage comes to the rescue. Code Coverage tell us how much of our feature is covered by unit tests, whether we've missed a path, for example we've covered the `if` condition but not the `else` condition. Let's add a new script to the scripts section of our `package.json` file. ```json { // other scripts "coverage":"vitest run --coverage" } ``` With this we can run `npm run coverage` locally and will create a `coverage` directory for us in the root of the project and we'll get a table of files along with their code coverage: | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | | ---------------- | ------- | -------- | ------- | ------- | ----------------- | | All files | 100 | 100 | 100 | 100 | | | app.component.ts | 100 | 100 | 100 | 100 | | When we run this it might prompt to install a supporting package this can be either `coverage-v8` (the default) or `coverage-instanbul` package, if not we can install them manually using: ```bash # For v8 npm i -D @vitest/coverage-v8 # For istanbul npm i -D @vitest/coverage-istanbul ``` We can set the `provider` type in the `coverage` section in the `vite.config.mts` file: ```typescript export default defineConfig(({ mode }) => { return { plugins: [angular()], test: { globals: true, environment: "jsdom", setupFiles: ["src/test-setup.ts"], include: ["**/*.spec.ts"], reporters: ["default"], coverage: { provider: "v8", reporter: ["text", "json", "html"], }, }, define: { "import.meta.vitest": mode !== "production", }, }; }); ``` To run `code coverage` in our CI environment we need to adjust our `test:ci` script to include a reporter, here I've used `junit` and `junit.xml` and there is a handy plugin for `Azure Devops` to read the `junit.xml` file which will import the file and create a nice dashboard for us. ```json script:{ // other scripts "test:ci": "vitest run --reporter=default --reporter=junit --outputFile=reports/junit.xml", } ``` In `Azure devops` we can add the `PublishTestResults` task in our build pipelines and would have the task run after our unit tests have completed. When we run our unit tests the results will be displayed in the terminal (unless running in a CI environment), but if you prefer to see the results in a browser this is also possible, first we need to install the ui tooling: ```bash npm i @vitest/ui ``` You will need to match the version of `@vitest/ui` to the same version of `vitest` installed on your system, once installed we can start it by running `vitest --ui` from the terminal, if we add the `html` to the `reporters` array in our `vite.config.ts` file, this will create an `html` directory with all the files. ```typescript export default defineConfig(({ mode }) => { return { plugins: [angular()], test: { globals: true, environment: "jsdom", setupFiles: ["src/test-setup.ts"], include: ["**/*.spec.ts"], reporters: ["default", "html"], // add html into this array. coverage: { provider: "v8", reporter: ["text", "json", "html"], }, }, define: { "import.meta.vitest": mode !== "production", }, }; }); ``` ## Conclusion In this post we looked at installing and setting up Vitest, a unit test runner in for Angular applications based on Vite, we also added code coverage to our test runs to see how much code coverage our unit tests cover, we also learnt how we can set this up in our CI in Azure Devops. If you would like to see a working project then I have a repo on GitHub [Vitest test application](https://github.com/DuncanFaulkner/vitest-app?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/Screenshot-2024-09-17-at-15.24.51--2---1--1.jpg) ### AI in Modern Angular Workspaces – The Future is Now! URL: https://www.angularspace.com/ai-in-modern-angular-workspaces-the-future-is-now/ Last updated: 2024-10-23T14:12:53.000Z The intersection of AI and front-end development is reshaping how we build, test, and manage our applications. At the end of this month I will give a talk at 4CEE about **AI in modern Angular workspaces**. This post will walk you through the core themes, insights, and takeaways from my presentation, while showcasing how AI is transforming the way we work with Angular. ## Why AI in Angular? Well, forget Angular. Why AI in coding in general. We still love to code but we don't want to lose time on simple time-consuming stuff like writing utilities, refactoring projects, moving elements in our architecture around, writing unit-tests etc etc. I started with simple prompts, then I realised, the more context, the better the result. And then I realised, the more context, the quicker the LLM started to hallucinate 😅 It was time to learn some proper prompt engineering, like few shot prompting, COT, TOT, prompt chaining etc... My journey started with **prompt engineering**, gradually evolving into more complex concepts such as **retrieval-augmented generation (RAG)** and **building AI agents** within Angular workspaces. If you're curious about how AI can write unit tests, refactor code, and even generate speaker notes or LinkedIn posts, stay tuned. ## Key Concepts Covered ### 1\. **AI and Large Language Models (LLMs)** LLMs such as GPT-4 are powerful enough to generate human-like text, but they are also changing how we code. By **understanding how to craft effective prompts**, developers can guide AI models to write code snippets, generate forms, and even automate certain tasks that usually take hours. ### 2\. **Prompt Engineering: A Developer’s Superpower** **Prompt engineering** is all about crafting inputs that help guide AI models towards desired outputs. A well-designed prompt can make all the difference between useful results and “AI hallucination” (when AI gives irrelevant or inaccurate answers). For instance, I can instruct AI to build a dynamic Angular form with complex logic and validation, just by using the right prompts. The results weren’t just decent – they were **mind-blowing**. ### 3\. **RAG: Retrieval Augmented Generation** If you work with large data sets, it's impractical and costly to feed every possible detail into an AI prompt. That’s where **RAG** comes in. This technique retrieves relevant documents or data chunks from a database and then feeds them into an AI model, allowing it to provide accurate and context-aware responses. For example, instead of asking the AI about the thousands of products in a catalog, RAG fetches just the relevant ones and improves efficiency, cost, and speed. ### 4\. **AI in Angular: Automating Dev Processes** By integrating AI into our Angular workspaces, we can streamline many development processes. Here’s a glimpse into what’s possible: - **Automating utility functions**: AI generates utility functions based on project requirements, saving developers hours of repetitive coding. - **Unit tests**: Imagine a world where unit tests are generated automatically based on your code changes. I recently fixed 3 bugs in one minute by doing the following: Asking LLM to write unit test, executing the unit test, telling LLM which tests failed, and replacing my logic with the updated logic of the LLM. INSANE! - **Refactors**: AI can help refactor large codebases consistently, ensuring code follows the best practices and project architecture guidelines. - **Code consistency**: The AI can assist by the workspace's architecture rules, making sure elements like components, services, guards, and types all follow specific file naming conventions and folder structures. ### 5\. **Nx: Scaling Angular Workspaces with AI** For those working with **Nx**, the powerful tool for managing large monorepos, AI makes it even better. In my talk, I will showcase how **custom Nx generators** can be combined with AI models to generate code, enforce architecture rules, and make developers’ lives easier by reducing complexity and providing consistency across projects. By doing this, AI can help **keep workspaces consistent, enforce architecture rules**, and make refactoring a breeze. ### 6\. **Building an AI-powered Workspace** One of the most exciting aspects of my talk is how you can build an **AI-powered workspace** in Angular. Imagine an AI agent that understands your codebase, the components, services, and libs it contains. You could ask it to write a form, generate reusable components, or even optimize your code – all with AI understanding your architecture. ## The Future of AI and Angular: Embrace the Change The core message of my talk is simple: **don’t ignore AI**. It is reshaping our industry, and as developers, we should **embrace it**. While some might worry about job displacement, the truth is we can't predict it. What we do know is that AI will enhance our capabilities, allowing us to focus on the creative and complex tasks that truly matter. Here are a few tips for embracing AI in your workflow: - **Start small**: Use AI tools like ChatGPT for simple tasks such as generating utility functions or writing tests. - **Learn prompt engineering**: Knowing how to craft the right input can significantly improve the quality of AI-generated code. - **Use Markdown for everything**: Whether it's documentation, blog posts, or even generating slides, AI thrives in well-structured environments like Markdown. - **Integrate AI into workspace**: Use NX generators to call LLM's and work as AI agents for you If you are interested, I'm organizing an online in-person workshop 13th of December 2024\. We will build an AI agent together that codes for us and writes directly to our codebase, we will build a RAG system with a chat assistant that can talk to our content. You can check it out here: [https://www.simplified.courses/ai-workshop](https://www.simplified.courses/ai-workshop?ref=angularspace.com) ## ## ### Bad Practice Good Morning URL: https://www.angularspace.com/bad-practice-good-morning/ Last updated: 2024-10-19T06:53:17.000Z I started posting a Good morning daily on my social media platforms I use (LinkedIn and X). But not a regular good morning 😄. Bad Practice Good Morning has caught a lot of attention and positive vibes. So I decided to summarize entire week and share it with you here as well! _This post is for subscribers only._ ### Angular News Recap URL: https://www.angularspace.com/angular-news-recap/ Last updated: 2024-10-18T19:17:21.000Z I promised to send this out a long time ago!!! Time to make it happen. A lot is happening, and it’s hard to keep up, but it’s important to be aware so we can adjust our practices for the future. _This post is for subscribers only._ ### Angular Friction Points URL: https://www.angularspace.com/angular-friction-points/ Last updated: 2024-10-17T13:51:13.000Z Angular is a very opinionated framework, with multiple different rules and solidified approaches that help developers easily navigate decisions within their projects. However, as with any tool, there is some variability, and the core team cannot, and, more importantly, *will not* make every single decision in developer's stead. In some situations, two or more tools or approaches are available, and it is up to the developer to choose the one that best fits their needs. This can be done either via subjective opinions ("I like using different templates", or "I don't want RxJS"), via the "consistency" argument (this codebase already uses Reactive forms, we should always stick to them), or on a case-by-case basis ("here it makes sense to use `ngModel`, but here a Reactive form is more appropriate because of the validations"). These situations often cause heated debates online, and there is no one definitive answer to all the dilemmas. We will soon discover that more often than not, adherence to a specific rule is the key to a highly maintainable codebase, rather than some sort of "higher truth" which compels us to always use the same approach. So, in this article, we will discuss different friction points that cause these debates, dive into both the upsides and downsides of both approaches, and provide some guidance on how to choose the right tool for the job. Notice that those sections will be titled "How to choose?" rather than "What to choose?", because we are going to provide advice to help you decide, rather than decide in your place. Let's begin the discussion with some of the simple issues and move on to more complex ones. ## NgClass vs class attribute In Angular templates, we can use either the `ngClass` directive or the `class` attribute to conditionally apply CSS classes to an element. Both have their merits. ### NgClass **Upsides:** While both `[class]` and `NgClass` can be used to apply multiple classes to an element: ```html
...
``` `NgClass` also supports keys with spaces: ```html
...
``` While the same will not work with `[class]` and neither of the classes will be applied. **Downsides:** `NgClass` can be more verbose than the `class` attribute if we use it to apply a single dynamic class. Also, it is a directive, which must be imported into the component. In addition to this, `NgClass` also supports binding to a JavaScript `Set` of string to represent classes, which, albeit rare, can prove useful in some situations. ### Class attribute **Upsides:** `class` attribute is a native HTML attribute, so in this case, we will be using Angular's binding mechanism without the need to import a directive. It can also be less verbose when applying a single class. Take a look at this: ```html
...
``` And then compare it to this: ```html
...
``` As we can see, `class` binding is a bit shorter. **Downsides:** `class` attribute can be either bound to a single class, or be bound to an object with boolean values, with keys that do not contain spaces. It cannot be used with other formats, in contrast to `ngClass`. **How to choose?** In this scenario, it is obvious that only two realistic ways of thinking exist: 1. Choose one and consistently apply it to all cases 2. Use `class` binding for simple cases and `ngClass` for complex ones or ones involving data structures like `Set` Best course of action will be to pick one approach that you are most comfortable with, and stick with it. Now, onto our next simple case. ## "inject" function vs Constructor DI Since Angular v14, we have been able to use the `inject` function to inject dependencies into our components instead of relying on the constructor. Lots of developers already abandoned constructor DI, and Angular has switched lots of its building blocks (like interceptors and guards) to a functional approach with "inject". However, the discussion is still somewhat alive, with some developers unwilling to make the switch. Let us figure out how to make a decision. ### "inject" function **Upsides:** The `inject` function is less verbose, does not require a constructor function at all, and can be used everywhere (within injection contexts, so it must be called from a constructor or DI factory function), not just classes. It also makes it simpler to apply DI lookup options. Consider this example: ```typescript @Component({...}) export class MyComponent { constructor( @Optional() @SkipSelf() private readonly someService: SomeService, ) {} } ``` And compare it to this: ```typescript @Component({...}) export class MyComponent { private readonly someService = inject(SomeService, {optional: true, skipSelf: true}); } ``` The `inject` function also makes it easier to deal with typings of`InjectionToken`\-s. Here we can see the difference: ```typescript export class MyService { constructor( @Inject(SOME_TOKEN) private readonly token: SomeTokenType, ) {} } ``` VS ```typescript export class MyService { private readonly token = inject(SOME_TOKEN); // this will implicitly have the type SomeTokenType } ``` It is important to note that the previous example (with the `@Inject` decorator) relies on an experimental TypeScript feature called `experimentalDecorators`, which might get dropped in the future as ECMAScript specification for the future decorators implementation is different from what we have in TypeScript right now (decorators on constructor arguments are not supported). Read more about this [here](https://github.com/tc39/proposal-decorators?ref=angularspace.com). This fact makes it more preferable to just use `inject`. **Downsides:** `inject` function can sometimes cause confusion when used outside injection contexts. We can call it within any function, but that function ultimately has to be called from a component or service constructor. It is important to note that we cannot classify this as a limitation, because constructor DI, requires, well, a constructor anyway, so, in fact, we can run into issues because we actually *expanded* the capabilities of dependency injection. The error that we might encounter is easy to debug and fix. A more real downside of this approach is unit testing services. With constructor DI, we can easily created an instance of a service to test, and provide mock values as its dependencies via the constructor. Look at this: ```typescript describe('SomeService' () => { let instance: SomeService; beforeEach(() => { instance = new SomeService(otherServiceMock); }); }); ``` We can easily do this with classes that use constructor DI. However, with the `inject` function, we have to run the tests within the injection context, which is quite verbose: ```typescript describe('SomeService' () => { let instance: SomeService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ {provide: OtherService, useValue: otherServiceMock}, ], }); TestBed.runInInjectionContext(() => { instance = inject(SomeService); }); }); }); ``` This is, of course, a visible downgrade. However, we must note two important things here. 1. This only applies to services in unit tests. If you don't write unit tests, you won't encounter this issue. If you unit tests components and directives, which have to be initialized with `TestBed`, you will not run into this even if they use the `inject` function. 2. Using `TestBed` when testing all Angular building blocks is the recommended and preferred way, as `TestBed` mimics the behavior of the framework as close as possible. ### Constructor DI **Upsides:** Constructor DI, as we have seen, is better for unit testing services. It is also a bit more familiar to developers that are used to dependency injection from other frameworks and languages, like .NET and such. Unfortunately though, this is where the upsides of this approach end. **Downsides:** As obvious from the examples, constructor DI is not as flexible as the `inject` function, it needs a constructor, and is not very good for typings of `InjectionToken`\-s. **How to choose?** Here we, again, have two options: 1. Just adopt and stick to the `inject` function. The only downside we have seen is not that big of a deal to drop this approach 2. Use `inject` everywhere other than services for simpler unit testing services. Can be seen as a valid approach, but can also complicated things. "Stick to constructor DI" is no longer an option, because as mentioned, several building blocks like guards and resolvers have been switched to functional approaches with `inject`, and their class counterparts have been deprecated. It would seem that the Angular team itself favors the `inject` function, and will provide a migration schematic in v19 to seamlessly adopt the new approach. Now, let's move on to a really controversial one. ## Single file components vs separate templates As we know, Angular components allows us to either provide an inline string as a component template, or use a separate HTML file. Since its inception, this has been one of the hot questions in the framework, so let us quickly decouple the pros and cons. ### Separate templates **Upsides:** The HTML code is stored separately from the component code, and looks less like magic. Some IDEs might have better tooling for this approach rather than inline templates (worth noting that the Angular Language Service usually mitigates this problem). **Downsides:** Separate templates can give developers a false sense of security, as they might think they are separating concerns all the while adding more and more code to the templates. They can also be harder to search through, as IDEs can sometimes lag when switching between files. > Note: If you want to use separate templates, feel free to explore [this extension](https://marketplace.visualstudio.com/items?itemName=erhise.vs-ng-quick-switch&ref=angularspace.com) which adds hot keys that allow to quickly navigate between a component's template, styles and tests. ### Single file components **Upsides:** With SFCs, we will have way less files and folders in the application structure. SFCs can also be easier to navigate, as everything related to a component is also in the same file. **Downsides:** SFCs can be seen by some as a violation of the Single Responsibility Principle, as they combine 3 different languages (HTML, TypeScript, CSS) into one file. This is of course debatable, because SFC advocates will say that "concerns" here relates to actual business concerns like features and pages, and not the underlying technology that is used. At the end of the day, HTML, TS and CSS files of the same component are usually edited together. **How to choose?** This is a very subjective question, and it depends on the team and personal preferences. However, we can establish ground rules for both approaches to make them work: 1. Choose SFCs and limit the length of a single component file. This is easier to achieve in this case, as we only have one file to keep track of, and it will compel us to keep our components short and concise. 2. Choose separate templates and using static code analyzer tools to enforce file length. In this case, the main concern is that separately, TS, CSS and HTML files will be short, but together, as a unit, they might contain way too much logic and actually inadvertently violate the Single Responsibility Principle. This can be mitigated by adopting a policy of keeping *all* files short. Now, let's move to a friction point that is so subjective, we can surely say it does not have a definitive answer. ## Template-driven forms vs Reactive forms In Angular, as we now, we have two ways of handling forms, using `ngModel` or Reactive Forms with `FormControl`\-s. Let's take a look at the pros and cons of each approach, but keep in mind that this one often really comes down to personal preferences. ### Template-driven forms **Upsides:** Template-driven forms are easier to use and get familiar with, as we can use the `ngModel` directive to bind the form to any property of the component. This has become even significantly easier with the introduction of signals: ```typescript @Component({ template: ` {{ text() }} `, imports: [FormsModule], }) export class MyComponent { text = input(); } ``` As we can see, all we need to do is to import the `FormsModule` and drop the `ngModel` directive somewhere. We than can use the signal (or any other property) to perform any actual business logic that we have. In this scenario, the template itself is used to describe what is going on with our form (hence template-driven forms). **Downsides:** In the case of signals, it can be hard to extract the value of an object that represents our form. Consider this: ```typescript @Component({ template: ` `, imports: [FormsModule], }) export class MyComponent { form = { email: signal(''), password: signal(''), }; submitForm() { // this can become verbose if we have a lot of fields const formValue = { email: this.form.email(), password: this.form.password(), }; // submit the form } } ``` In the case of Reactive Forms, we can just use the `form.value` property to get the value of the form. Another downside is the validations. With template-driven forms, we use native HTML validations, and for custom validations, we have to use directives, which increases the amount of code we need to write. ```typescript @Directive({ selector: '[appPositiveNumber]', providers: [{provide: NG_VALIDATORS, useExisting: PositiveNumbersValidatorDirective, multi: true}], }) export class PositiveNumberValidatorDirective implements Validator { validate(control: AbstractControl): ValidationErrors | null { return (+control.value) > 0 ? null : {positiveNumber: true}; } } @Component({ template: ` `, imports: [FormsModule, PositiveNumberValidatorDirective], }) export class MyComponent { number = signal(0); } ``` But with a reactive form, the validator will be a simple function: ```typescript function positiveNumberValidator(control: AbstractControl): ValidationErrors | null { return (+control.value) > 0 ? null : {positiveNumber: true}; } @Component({ template: ` `, imports: [ReactiveFormsModule], }) export class MyComponent { number = new FormControl('', {validators: [positiveNumberValidator]}); } ``` ### Reactive forms **Upsides:** Reactive forms are clearly defined as forms, with everything that we need to know about them written in a single precise location. including validators, initial value, disabled state, and much more. In the template, we only need to bind the controls to specific inputs, and the rest is handled by the framework. Another upside is that we can easily use the `form.value` property to get the value of the form. We can use the built-in `valueChanges` and `statusChanges` observables to listen to changes in the form (this is not an issue when using signals for template driven forms), and much more. Finally, a more "abstract" upside of embracing RxJS is making sure that developers write more declarative code, instead of running around a component trying to keep different parts of the state in sync. This can be especially useful when adopting state management solutions like NgRx. **Downsides:** Reactive forms are obviously more verbose, and while they are allow us to clearly define a for with everything that is needed for it to function, it is still a wrapper around a value, which means we might have to perform extra actions to keep the value in sync with the form itself. For instance, if we want to add a control depending on another controls value, it can be relatively easy with template-driven forms that contain signals. Take a look at this example, where choosing a certain topic adds an option to choose a subtopic, done with a template-driven form: ```typescript @Component({ template: `
@if (form().subtopic) {
}
`, }) export class AddQuestionComponent { private readonly topicService = inject(TopicService); defaultControls = { title: signal(''), topic: signal(null), }; hasSubtopic = computed(() => { return !!this.topics().find( (topic) => topic.id === +(this.defaultControls.topic() ?? 0) )?.hasSubtopic; }); form = computed(() => { return ({ title: this.defaultControls.title, topic: this.defaultControls.topic, ...(this.hasSubtopic() ? { subtopic: signal(null) } : {}), }); }); topics = toSignal(this.topicService.getTopics(), { initialValue: [] }); subTopics = toSignal( toObservable(this.defaultControls.topic).pipe( filter((topicId) => this.hasSubtopic()), switchMap((topicId) => this.topicService.getSubTopics(topicId!)) ), { initialValue: [] } ); } ``` Here, we can define the form with computed value, and account for a specific topic that might have subtopic options. With a reactive form, however, the logic will be much more complicated: ```typescript @Component({...}) export class AddQuestionComponent implements OnInit { private readonly topicService = inject(TopicService); form = new FormGroup({ title: new FormControl(''), topic: new FormControl(null), }); ngOnInit() { // here, we need to separately subscribe to form changes // and add/remove the control manually this.form.get('topic')?.valueChanges.subscribe((topicId) => { if ( topicId && this.topics.find((topic) => topic.id === topicId)?.hasSubtopic ) { this.form.addControl('subtopic', new FormControl(null)); } else { this.form.removeControl('subtopic'); } }); } topics = toSignal(this.topicService.getTopics(), { initialValue: [] }); subTopics = toSignal( toObservable(this.form.get('topic')?.value).pipe( filter((topicId) => this.form.get('subtopic')?.value), switchMap((topicId) => this.topicService.getSubTopics(topicId!)) ), { initialValue: [] } ); } ``` This looks way more complicated than what we have with template-driven forms, and can become more and more complex as the form receives more input controls. As we can also see, the API for Reactive forms is very imperative. To disable an input, we need to call `disable()` on the control, and to enable it, we need to call `enable()`. We cannot just bind to a property in the template like we can do with template-driven forms, which might result in hacky code. Often we might need to toggle a disabled state based on an Observable value, with template-driven forms we can just bind it to the `disabled` attribute using the `async` pipe, but with reactive forms, we have to subscribe to it and use the `disabled` property of the control, which can make our code quite ugly, especially if we have lots of scenarios like this. **How to choose?** Now, this one often really does come down to personal preference. However, this is the case where consistency in rules is key. We have several equally valid options: 1. Choose reactive forms and stick to it. 2. Choose template-driven forms and stick to it. 3. Use template-driven forms for simple cases with defined rules and boundaries (for instance, if we have no validations, let's just use a template-driven form in that case), and reserve reactive forms for far more complex scenarios with multiple inputs and cross-cutting validations. Now, let's move on to the final topic of the day, one that might actually turn out not to be a dilemma at all! ## RxJS vs Signals RxJS has been a part of Angular since its very beginning, while signals are relatively new. But this already resulted in a big debate online; RxJS was divisive for Angular developers even before signals, but now even more users are eager to ditch it. But is this warranted, or actually a hasty move? ### RxJS **Upsides:** RxJS is a very powerful library, and it is used in many places in the framework. We already touched it with Reactive Forms, for instance, and, of course, it is used in many other core framework features, like the `HttpClient` and `Router`. RxJS has many operators, different approaches for different tasks, and can be very, very flexible. When done correctly, it can produce beautiful, easily readable and maintainable code. It can be both synchronous and asynchronous. Really, our imagination might be the only limit here. **Downsides:** While RxJS is extremely powerful, it is also intimidating. It has more than 100 operators, different caveats like cold and hot Observables, and potential issues when mishandled like memory leaks. Also, because it is an independent library, there are many multiple ways of handling the same thing, and differing opinions on usage, like "should you subscribe or use the async pipe?" and so on. In addition to what we said, it is worth mentioning that RxJS is not a silver bullet, and can unnecessarily complicated synchronous code. This is where signals come in to shine. ### Signals **Upsides:** Signals are very simple, its easy to create one, derive from another, compose them, and react to their changes via `effect`\-s. They are also way more predictable than Observables, which makes it easy to reason about them. Their potential for abuse is lower than RxJS. **Downsides:** Signals are synchronous, which is both an upside and a downside. Of course, synchronous things are way easier to deal with - we can read their current value whenever we want, and there are no race conditions. However, we still *need* asynchronous functionality in our applications (HTTP calls say hello), and signals are not really suited for those things (by design). Signals also lack the versatility of operators that allow to transform, time, filter and combine them in more than one way. All of that responsibility is delegated to us, the developers. So, what gives? **How to choose?** This section will be significantly longer than the others. First of all, we need to understand if there is actually any animosity between signals and RxJS. If we think about it for a while, we will realize that there is none. Signals are well-suited to contain reactive values - values that can be read and changed at any moments, but whose changes can also be tracked to make our application to react to them. RxJS is great for dealing with events (browser events, HTTP calls, storage events, Web Sockets and much more) in a way that allows us to control both the nature of the emitted items (filter, map, combine them) and the timing (debounce, timer, interval and so on). This makes it clear that they are certainly meant for somewhat different purposes, but their goals often intermingle. For instance, HTTP calls are asynchronous events, so RxJS is good, but the value that they return is a reactive data, so, signals might be great to store, use and react to them. This means it would be amazing if both existed within Angular apps and could easily "talk" to each other. So... that is exactly what the Angular team did. While slightly decreasing the dependency on RxJS, they also introduced the *"@angular/core/rxjs-interop"* package, which allows us to easily translate between signals and Observables to produce both simple and powerful code. Let's see a glimpse of that power in action: ```typescript @Component({ template: `
    @for(product of products(); track product.id) {
  • {{ product.name }} - {{ product.price }}
  • }
`, imports: [ FormsModule, ], }) export class ProductListComponent { private readonly productService = inject(ProductService); query = signal(''); // extract the result of the HTTP call into a signal products = toSignal( // switch to Observable world toObservable(this.query).pipe( // use the power of RxJS operators to introduce timing limits debounceTime(500), switchMap(query => this.productService.getProducts(query)), ), { initialValue: [] }, ) } ``` Here, as we can see, the combination of RxJS and signals allowed us to create a powerful abstraction over making a timed HTTP request, and in the end just expose the result as a signal to be used both within the template and in other parts of the component. With such advances, is it reasonable to pit signals and RxJS against each other? My own opinion would be no. Here is how we can approach the decision making process: 1. Simply use both RxJS and signals in your application. This is the most straightforward approach, but will require a learning curve. 2. Use RxJS exclusively. This suits for applications that are on older versions of Angular that do not yet have signals - sadly, a very huge chunk of existing apps. However, using Observables will make it easier to also adopt signals later on. 3. Use signals exclusively. If you really don't want to use RxJS, it is acceptable, and the framework is in general moving towards the direction of making RxJS optional (but better supported!). Also this works for really simple apps. I purposefully did not include the "don't use either", because usually not utilizing either of these tools is more of a situation, rather than a choice; an old project, which developers already created without RxJS. Whichever you use, keep in mind that RxJS will most probably never go away, and, as mentioned will most likely become even better synchronized with the framework. ## Conclusion Angular, as any other tools, is driven and used by humans, who, often, have very, very different opinions. This disagreements are inevitable, but should never be a deal-breaker for the tool, and should never hold us back from trying new things out, or changing our mind. If you are just starting your Angular journey, this discussions might sometimes feel quite overwhelming (what do I do?!), so, hopefully, this article gives you the tools to try and make your own, informed decisions. ## Small promotion As you see, some of the features that we discussed, like `inject` and signals, are quite new to Angular. The recent upheaval in Angular has caused many developers to be confused about what solutions to chose, how to implement them, and how to migrate their existing codebases to the most recent features. Thankfully, I have a response to this concern: very soon, my very first book is going into print! It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular?ref=angularspace.com). The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on [Twitter](https://twitter.com/Armandotrue?ref=angularspace.com) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/?ref=angularspace.com), where I will be posting whenever there are news or promotions available. P.S. Hey! Check out chapter 5 of my book to learn more about RxJS and Angular interoperability, and chapters 6-7 to dive deep into signals ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/Screenshot-2024-09-17-at-15.24.51--2--1.jpg) ### Angular's effect(): Use Cases & Enforced Asynchrony URL: https://www.angularspace.com/angulars-effect-use-cases-enforced-asynchrony/ Last updated: 2024-10-17T13:52:50.000Z Angular's Signals are designed with simplicity in mind, providing three core functions: `signal()` to create Signals,`computed()` for derived Signals, and `effect()` to handle side effects. The last one, `effect()`, stands out. While still in developer preview (unlike `signal()` and `computed()`, which became stable in v17), `effect()` has garnered a lot of attention in social media, blog posts, and community discussions. Many suggest avoiding it altogether, with some even saying it shouldn't exist. This article presents three key arguments: 1. **`effect()` has its righteous place and should be used where necessary.** 2. **The asynchronous nature of `effect()` means it should not be used to update Signals synchronously.** For these cases, `computed()` is the better choice, even if some edge cases lead to less readable code. 3. **The community discussion should move beyond stylistic debates** (such as declarative vs. imperative programming) and blanket statements like "Don't use `effect()`." Instead, the focus should be on understanding the real consequences of its asynchronous behavior, which can have significant impacts on applications. If you prefer watching a video over reading: [Video Version](https://youtu.be/WgdnW%5FnJuRA?ref=angularspace.com) - [Signals Primer](#signals-primer) - [computed() or effect(): A Matter of Style?](#computed-or-effect-a-matter-of-style) - [The Case for effect()](#the-case-for-effect) - [Examples for side effects](#examples-for-side-effects) - [Examples with asynchronous Signal updates](#examples-with-asynchronous-signal-updates) - [effect()'s Achilles' Heel: Enforced Asynchrony](#effects-achilles-heel-enforced-asynchrony) - [The Reset Pattern](#the-reset-pattern) - [linkedSignal](#linkedsignal) - [Summary](#summary) ## Signals Primer If you're already familiar with Signals, feel free to [skip](#computed-or-effect) this section. A Signal is a container for a value. To create one, we use the `signal()` function. To read the value, we call the Signal like a function. To update the value, we use the `set()` or `update()` methods. ```typescript const n = signal(2); console.log(n()); // 2 n.set(3); console.log(n()); // 3 n.update((value) => value + 1); console.log(n()); // 4 ``` Updates to a Signal must be immutable. If the Signal holds an object, the new value must have a different object address, e.g., by creating a shallow clone. `computed()` creates a derived value, meaning it depends on other Signals and recalculates its value whenever those Signals change. Signals created with `signal()` are of type `WritableSignal`, while those created with `computed()` are of type `Signal`. A signal that notifies another is called a *producer*, while the one that depends on it is called a *consumer*. ```typescript const n = signal(2); const double = computed(() => n() * 2); console.log(double()); // 4 n.set(3); console.log(double()); // 6 ``` If we just want to execute code when one or more Signals change, we use the `effect()` function. Its usage is similar to `computed()` but doesn't return a new Signal. An `effect()` runs asynchronously, at least once initially, and then whenever its producer notifies it. ```typescript const n = signal(2); effect(() => console.log(n())); window.setTimeout(() => { n.set(3); n.set(4); }, 0); // console output 2: (asynchronous execution of effect) // console output 4: (asynchronous execution of effect) ``` > Note that there's no output for the value 3\. This happens because multiple synchronous changes happened, and the `effect()` only captures the final state after the synchronous execution completes. Be aware of implicit tracking: if your `effect()` calls a method or function, all Signals used within it will be automatically tracked. To avoid that, wrap that code with `untracked`. ```typescript effect(() => { const value = someSignalWeWantToTrack(); untracked(() => { someService.doSomething(value); }); }) ``` For more information, head to the [official Angular documentation](https://angular.dev/guide/signals?ref=angularspace.com). ## computed() or effect(): A Matter of Style? There's a strong tendency to caution against using `effect()`. On social media, some even argue that you should never use it, though this doesn't hold up in real-world scenarios. The official Angular documentation states: "Avoid using effects for the propagation of state changes." The examples often presented are simple and are often cases where `computed()` could easily replace `effect()`. I'd argue that this is obvious. Years of Angular + RxJS development have ingrained in us that this is an anti-pattern: ```typescript @Component({ // ... template: `Double: {{ double }}`, }) class DoubleComponent { n$ = new BehaviorSubject(2); double = 0; constructor() { this.n$.subscribe((value) => (this.double = value * 2)); } } ``` The computation of `double` here is a side effect. The best practice is to create derived `Observable` streams and avoid direct subscriptions. ```typescript @Component({ // ... template: `Double: {{ double$ | async }}`, }) class DoubleComponent { n$ = new BehaviorSubject(2); double$ = this.n$.pipe(map((value) => value * 2)); } ``` This code is more declarative. We don't need to explicitly subscribe, calculate, and assign the value. Instead, we define the calculation and connect it to the source. This approach makes the code easier to read and maintain. This example also presents a problem when the component uses `OnPush` as change detection strategy. `OnPush` is a performance optimization where components need to be explicitly marked as dirty in order to be checked and therefore updated in the DOM. While the `async` pipe handles this automatically, a manual subscription does not. The same principle applies to `effect()` and `computed()`. The `effect()` is imperative, while `computed()` is declarative. ```typescript @Component({ // ... template: `Double: {{ double }}`, }) class DoubleComponent { n = signal(2); double = 0; constructor() { effect(() => (this.double = this.n() * 2)); } } ``` Why would we use `effect()` here if `computed()` is available? ```typescript @Component({ // ... template: `Double: {{ double() }}`, }) class DoubleComponent { n = signal(2); double = computed(() => this.n() * 2); } ``` Most of us wouldn't even consider using an `effect()` in this scenario. Many discussions focus too much on the imperative vs. declarative. This is a stylistic debate, as using `effect()` in these cases doesn't harm your application. Therefore, it can lead developers to think it's "safe" to use `effect()` to update other Signals. In some cases, `effect()` is even more readable. The real issue is the asynchronous nature of the `effect()`, which can cause significant bugs. An example will follow later, but before diving into those examples, let's first take a look at the valid use cases for `effect()`. ## The Case for effect() The "Don't use `effect()`" trend has led to some confusion. Whereas some developers might not see the risk of `effect()` others may avoid `effect()` even when it's the best and most appropriate choice. Here are some noteworthy tweets from highly respected members of the Angular community: - [https://x.com/tomastrajan/status/1835596353143021850](https://x.com/tomastrajan/status/1835596353143021850?ref=angularspace.com) - [https://x.com/brandontroberts/status/1836815585776160805](https://x.com/brandontroberts/status/1836815585776160805?ref=angularspace.com) - [https://x.com/Nartc1410/status/1836066122904244443](https://x.com/Nartc1410/status/1836066122904244443?ref=angularspace.com) The most common uses cases for `effect()` are: 1. **"Signal-exclusive" side effects**: When reacting to a Signal's change where the immediate outcome is not a derived Signal. 2. **asynchronous changes to Signals**: When `effect()` updates another Signal, but first has to fetch data from a server. ### Examples for side effects A common example of using `effect()` is logging a Signal change or synchronizing data with local storage: ```typescript @Component({ // ... }) class DoubleComponent { n = signal(2); private logEffect = effect(() => console.log(n())); private storageSyncEffect = effect(() => localStorage.setItem("n", JSON.stringify({value: n()}))); } ``` When interacting directly with the DOM, `effect()` is also the right tool. For example, connecting a Signal to chart data: ```typescript export class ChartComponent { chartData = input.required(); chart: Chart | undefined; updateEffect = effect(() => { const data = this.chartData(); untracked(() => { if (this.chart) { this.chart.data.datasets[0].data = data; this.chart.update(); } }) }); // code for creating the chart } ``` As of this writing, we know that Angular 19 will introduce subtle changes to `effect` timing, which could impact the use of effect when dealing with DOM access. Additionally, new utility functions are on the way, making it easier to handle such scenarios. Another common example is form synchronization: ```typescript export class CustomerComponent { customer = input.required(); formUpdater = effect(() => { this.formGroup.setValue(this.customer()); }); formGroup = inject(NonNullableFormBuilder).group({ id: [0], firstname: ["", [Validators.required]], name: ["", [Validators.required]], country: ["", [Validators.required]], birthdate: ["", [Validators.required]], }); } ``` As you can see, the recurring pattern is that all these examples react to Signal changes but don't update other Signals. This is similar to how we use `Observable`, where we had side effects in `subscribe()` or the `tap()` operator: ```typescript export class ChartComponent { chartData$ = inject(ChartDataService).getChartData(); chart: Chart | undefined; constructor() { this.chartData$ .pipe( tap((data) => { if (this.chart) { this.chart.data.datasets[0].data = data; this.chart.update(); } }), takeUntilDestroyed(), ) .subscribe(); } // code for creating the chart } ``` ### Examples with asynchronous Signal updates Perhaps the most common use case for `effect()` is when a Signal changes and you need to fetch data asynchronously before updating another Signal. For instance, take the following example where a `Signal` tracks a customer `id` from route parameters. Based on this `id`, you need to retrieve customer data from a server and update another Signal with the response: ```typescript @Component({ // .. template: ` @if (customer(); as value) { } ` }) export class EditCustomerComponent { id = input.required({transform: numberAttribute}); customer = signal(undefined); customerService = inject(CustomerService); loadEffect = effect(() => { const id = this.id(); untracked(() => { this.customerService.byId(id).then( (customer) => this.customer.set(customer) ); }) }); } ``` In this example, `loadEffect` listens for changes to the `id` Signal, triggers an asynchronous fetch of customer data, and updates the `customer` Signal once the data is available. It may seem like `loadEffect` is setting a derived value, but since there's an asynchronous task involved, `computed()` isn't an option. `computed()` requires the function to return a value immediately, which isn't possible here. This loading mechanism could also be handled in a service, but that would just move the use of `effect()` to another place. If you have a large application where much data fetching depends on route parameters and you're already using `effect()` for this purpose, it's perfectly fine. Currently, your only options for reacting to Signal changes are `computed()` and `effect()`. If `computed()` doesn't work, `effect()` is the right choice. --- It's worth mentioning that when it comes to asynchronous tasks, there’s always the elephant in the room: RxJS. While RxJS can be a powerful tool for managing async workflows, this article focuses on the role of `effect()`. I’ll touch on RxJS in more detail in another article. If you want to go with an `Observable`, you must first convert the Signal to an `Observable`. For that, you'd use `toObservable()`, but guess what? It uses an `effect()` internally. Let's just keep in mind that when it comes to managing asynchronous race conditions, there is no way around RxJS. --- Before we continue, let's consider forcing our way to a `computed()`. It's possible, but the code would look like this: ```typescript @Component({ selector: "app-edit-customer", template: ` @if (customer(); as value) { } {{ loadComputed() }} `, standalone: true, imports: [CustomerComponent], }) export class EditCustomerComponent { id = input.required({transform: numberAttribute}); customer = signal(undefined); customerService = inject(CustomerService); loadComputed = computed(() => { const id = this.id(); this.customerService.byId(id).then((customer) => this.customer.set(customer)); }); } ``` What's the difference? First, we've generated a Signal of type `void`, which isn't particularly useful, and your fellow developers might not know what to do with a Signal of no value. Second, this only works because `loadComputed` is used in the template to keep the Signal alive. Unlike `effect()`, a Signal needs to be called within a reactive context, such as a template, to become reactive. We can all agree that using `computed()` in this case is not ideal. ## effect()'s Achilles' Heel: Enforced Asynchrony Here’s where real issue with `effect()` comes in. Unlike `computed()`, which runs synchronously, `effect()` enforces asynchronous execution. This can lead to serious bugs when immediate state updates are needed. Let's look at the following example: ```typescript @Component({ selector: "app-basket", template: `

Click on a product to add it to the basket

@for (product of products; track product) { }
@if (selectedProduct(); as product) {

Selected Product: {{ product.name }}

Want more? Top up the amount

} `, standalone: true, imports: [FormsModule, MatButton, MatInput], }) export default class BasketComponent { private readonly httpClient = inject(HttpClient); protected readonly products = products; protected readonly selectedProductId = signal(0); protected readonly selectedProduct = computed(() => products.find((p) => p.id === this.selectedProductId())); protected readonly amount = signal(0); #resetEffect = effect(() => { this.selectedProductId(); untracked(() => this.amount.set(1)); }); selectProduct(id: number) { this.selectedProductId.set(id); console.log(this.selectedProduct()?.name + " added to basket"); } updateAmount() { this.httpClient.post("/basket", {id: this.selectedProductId(), amount: this.amount()}).subscribe(); } } ``` The `BasketComponent` lists some products and allows the user to select one. After selecting a product, the user can update the amount for that product. The `#resetEffect` resets the amount to 1 whenever a new product is selected. We could have placed this logic inside the `selectProduct` method, but linking it to the `selectedProductId` Signal ensures that any changes to `selectedProductId` — even from other event handlers — will always trigger the reset. When the user switches to a different product, we want to send the selected product, along with the reset amount of 1, to the server. To achieve this, we add the following request inside `selectProduct`: ```typescript class BasketComponent { // ... selectProduct(id: number) { this.selectedProductId.set(id); console.log(this.selectedProduct()?.name + " added to basket"); this.httpClient.post("/basket", {id: this.selectedProductId(), amount: this.amount()}).subscribe(); } } ``` If we click on the first product, change the amount to something else, and then select a second product, we will see that the HTTP request still sends the amount from the first product. However, the input field correctly shows the reset value of 1. It’s not that the `#resetEffect` didn’t run — otherwise, the input field wouldn't have updated. The issue is a timing problem. An `effect()` runs asynchronously, whereas the event listener `selectProduct` runs synchronously. By the time the HTTP request is sent, the `#resetEffect` hasn’t even started executing, so the amount is still the old value. This is a severe bug. The user sees the correct value, but the server receives the wrong one. Even worse, if the user submits their basket thinking the amount is correct, they could end up paying more and receiving a larger quantity than expected. --- When working with Signals, it's important to understand the concept of a "glitch-free effect." This means that if a Signal changes multiple times synchronously, the frontend is only concerned with the final state. There's no need to update the DOM with intermediate states while a synchronous task is still in progress. Scheduling the `effect()` asynchronously makes sense because it ensures that all synchronous execution has completed. In this context, `effect()` and the template act as the "end" or "final consumer" of a Signal's reactive graph. On the other hand, `computed()` is part of the reactive graph itself but is not the final consumer, which is why `computed()` runs synchronously. --- So far, we've seen that `computed()` behaves to `effect()`, like `pipe()` behaves to `subscribe()` in RxJS. This is where the comparison with RxJS breaks down. In RxJS, a subscription would run synchronously, ensuring that everything stays in sync. --- We’ve identified the problem — now, what’s the solution? ## The Reset Pattern The reset pattern, introduced at [TechStackNation](https://youtu.be/aKxcIQMWSNU?si=XBSonrXsLR-rUXvZ&ref=angularspace.com), solves tricky synchronous Signal updates using `computed()`. In these cases, while `effect()` may seem simpler, the reset pattern ensures updates happen synchronously. The pattern places a nested Signal inside a `computed()`, initialized with a default value. These Signals act as triggers, and when they change, the `computed()` recalculates and updates the Signal synchronously. Here’s how the `#resetEffect` would be re-modeled using `computed()`: ```typescript class BasketComponent { protected readonly state = computed(() => { return { selectedProduct: this.selectedProduct(), amount: signal(1), }; }); } ``` As soon as the `selectedProductId` changes, the `computed()` is notified synchronously and is internally marked as dirty. The `selectProduct` method then reads the value of `amount` and gets the correct value back. For the sake of completeness, here is the final version of the `BasketComponent`: ```typescript @Component({ selector: "app-basket", template: `

Click on a product to add it to the basket

@for (product of products; track product) { }
@if (state().selectedProduct; as product) {

Selected Product: {{ product.name }}

Want more? Top up the amount

} `, standalone: true, imports: [FormsModule, MatButton, MatInput], }) export default class BasketComponent { private readonly httpClient = inject(HttpClient); protected readonly products = products; protected readonly selectedProductId = signal(0); readonly #selectedProduct = computed(() => products.find((p) => p.id === this.selectedProductId())); state = computed(() => { return { selectedProduct: this.#selectedProduct(), amount: signal(1), }; }); selectProduct(id: number) { this.selectedProductId.set(id); console.log(this.#selectedProduct()?.name + " added to basket"); this.httpClient.post("/basket", {id: this.selectedProductId(), amount: this.state().amount()}).subscribe(); } updateAmount() { this.httpClient.post("/basket", {id: this.selectedProductId(), amount: this.state().amount()}).subscribe(); } } ``` At first glance, and even after, the reset pattern seems like a lot of boilerplate. The `effect()` version is much more intuitive. ### linkedSignal Since this pattern is both common and unintuitive, the Angular team has [announced plans](https://blog.angular.dev/latest-updates-to-effect-in-angular-f2d2648defcd?ref=angularspace.com) to introduce utility functions, not just for the reset pattern but for others as well. As of this writing, Matthieu Riegler from the Angular team [has reported](https://x.com/Jean%5F%5FMeche/status/1845800168698085565?ref=angularspace.com) a new PR for a utility function called `linkedSignal()`, designed to handle the reset pattern. This function allows us to create a `WritableSignal` that reacts to changes in another signal. In this scenario, the following code: ```typescript class BasketComponent { protected readonly selectedProductId = signal(0); state = computed(() => { return { selectedProduct: this.#selectedProduct(), amount: signal(1), }; }); } ``` can be updated to: ```typescript class BasketComponent { protected readonly selectedProductId = signal(0); protected readonly amount = linkedSignal({ source: this.selectedProductId, computation: () => 0 }) } ``` Now, `amount` functions like a typical `WritableSignal`, but it will automatically reset whenever `selectedProductId` changes. ## Summary `effect()` has many valid use cases. Avoiding it in real-world applications will lead to poorer code quality. We can compare the relationship between `computed()` and `effect()` to the declarative style of using `pipe()` in RxJs versus placing side-effects directly in `tap()` or `subscribe()`. `effect()` runs asynchronously, however. For operations that synchronously modify other Signals, `computed()` is required — even if that means sacrificing some readability and maintainability. The risk of introducing bugs due to the asynchronous behavior of `effect()` is simply too high. Fortunately, the Angular team has already recognized these issues, and we can expect new utility functions, like `linkedSignal` to provide solutions for these cases. Other utility functions that use `effect()` internally, while protecting against misuse also exist. Examples include `toObservable()`, `rxMethod()` from [@ngrx/signals](https://ngrx.io/guide/signals/rxjs-integration?ref=angularspace.com), and `explicitEffect()` from [ngxtension](https://ngxtension.netlify.app/?ref=angularspace.com). These types of functions will likely become more common in the future, reducing the need for directly writing `effect()` in many situations. Common use cases for `effect()` include side-effects that don't result in changes to other Signals. It's also ideal for triggering asynchronous tasks, regardless of whether they eventually produce a new value for a Signal. Use `effect()`. It is a critical part of Signals. In case you are in doubt, let me give you a mnemonic: Whenever you have an `effect`, like this... ```typescript effect(() => { // side-effects, asynchronous or synchronous Signal updates }); ``` ...and you can wrap it into an asynchronous task like this... ```typescript effect(() => { Promise.resolve().then(() => { // side effect, asynchronous or synchronous Signal updates }) }); ``` ...you are fine. --- I'd like to thank the reviewers of this article: Alain Boudard, Dominik Pieper, and Matthieu Riegler from the Angular Team. Special thanks to Manfred Steyer for reviewing the original version, and to my GDE colleagues and Michael Egger-Zikes for the insightful discussions that led to this article. --- **Further Reading**: - Alex Rickabaugh at TechStackNation - Don't Use Effects 🚫 and What To Do Instead: [https://youtu.be/aKxcIQMWSNU?si=Kn31zD8R19AWScTQ](https://youtu.be/aKxcIQMWSNU?si=Kn31zD8R19AWScTQ&ref=angularspace.com) - Angular Documentation - Signals: [https://angular.dev/guide/signals](https://angular.dev/guide/signals?ref=angularspace.com) - Rainer Hahnekamp - Signals Unleashed, The Full Guide: [https://youtu.be/6W6gycuhiN0](https://youtu.be/6W6gycuhiN0?ref=angularspace.com) - Manfred Steyer - Blog Series on Signals: [https://www.angulararchitects.io/en/blog/angular-signals/](https://www.angulararchitects.io/en/blog/angular-signals/?ref=angularspace.com) - Angular Community - Discussion on Explicit Effect: [https://github.com/angular/angular/issues/56155](https://github.com/angular/angular/issues/56155?ref=angularspace.com) - Latest updates to effect() in Angular: [https://blog.angular.dev/latest-updates-to-effect-in-angular-f2d2648defcd](https://blog.angular.dev/latest-updates-to-effect-in-angular-f2d2648defcd?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/Screenshot-2024-09-17-at-15.24.51--1--1.jpg) ### 10x AI Assisted Programming PACKT GIVEAWAY URL: https://www.angularspace.com/10x-ai-assisted-programming-packt-giveaway/ Last updated: 2024-10-15T13:08:49.000Z This is a **MUST READ** for every developer in 2024 by Christoffer Noring, Anjali Jain, Marina Fernandez, Ayşe Mutlu, Ajit Jaokar via PACKT Publishing. _This post is for subscribers only._ ### 18 Interview Questions answered by Angular Experts [Live Post] URL: https://www.angularspace.com/18-interview-questions-answered-by-angular-experts-live-post/ Last updated: 2025-07-26T20:29:58.000Z **Michał Grzegorczyk**: Not long ago, [Daniel](https://twitter.com/DanielGlejzner?ref=angularspace.com) shared 18 interview questions on LinkedIn, Twitter and Discord. Questions crafted in a way to identify real Senior Angular developers. Using this opportunity we have asked Angular Experts to answer some of these. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/Screenshot-2024-10-07-at-18.55.11.png) **Daniel Glejzner:** Make sure to check back here regularly as this post is going to be updated with answers from even more experts in coming days/weeks :). I hope this is going to provide you necessary perspective on how answers in tech interview can vary. There is no one specific answer that is expected. Enjoy! ## But first, let's meet our Experts! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/Jz661MjL_400x400-1.jpg) ### **Kevin Kreuzer** > A trainer, consultant, streamer, content creator and senior front-end engineer with a focus on the modern web, as well as a Google Developer Expert for Angular & Web technologies. - [angularexperts.io](https://angularexperts.io/?ref=angularspace.com) - [https://twitter.com/kreuzercode](https://twitter.com/kreuzercode?ref=angularspace.com) - Checkout latest Angular Signals Masterclass e-book by Kevin here: [https://angularexperts.io/products/ebook-signals](https://angularexperts.io/products/ebook-signals?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/GIETC-bW_400x400-1-1.jpg) ### **Rainer Hahnekamp** > Experienced software developer and architect for enterprise applications. For over 15 years, he worked as a team lead for the well-known US confectionery manufacturer Mars. For his community activities, Rainer was recognized as a Google Developer Expert for Angular. - [rainerhahnekamp.com](https://www.rainerhahnekamp.com/en/?ref=angularspace.com) - [twitter.com/rainerhahnekamp](https://twitter.com/rainerhahnekamp?ref=angularspace.com) - [youtube.com/@RainerHahnekamp](https://www.youtube.com/@RainerHahnekamp?ref=angularspace.com) - [angularatchitects.io](https://www.angulararchitects.io/en/angular-workshops/?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/image-1.png) ### Alex Inkin > *Angular Google Developer Expert, front-end developer, tech writer, musician and open-source author. A*lex is one of the core creators and maintainers of Taiga UI. He has been instrumental in its development and promotion within the Angular community. - [x.com/waterplea](https://x.com/waterplea?ref=angularspace.com) - [taiga-ui.dev](https://taiga-ui.dev/?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/image-2-1.png) ### Tomas Trajan > A Google Developer Expert for Angular & Web Technologies working as a consultant and Angular trainer. Currently empowering teams in enterprise organizations worldwide by implementing core functionality and architecture, introducing best practices, sharing know-how and optimizing workflows. - [x.com/tomastrajan](https://x.com/tomastrajan?ref=angularspace.com) - [angularexperts.io](https://angularexperts.io/?ref=angularspace.com) - Latest book by Tomas about Architecture!! [https://angularexperts.io/products/ebook-angular-enterprise-architecture](https://angularexperts.io/products/ebook-angular-enterprise-architecture?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/image-3-1.png) ### Mateusz Łędzewicz > Principal Software Engineer Focused on @angular and Frontend technologies @ngLodz Organizer. Now, he serves as a Principal Consultant and Trainer at Lowgular, where he teaches Angular from scratch and helps clients enhance developer performance. His focus is on training developers to adopt best engineering practices and reduce technical debt, all while sharing his deep expertise in Angular. - [twitter.com/mat\_ledzewicz](https://twitter.com/mat%5Fledzewicz?ref=angularspace.com) - [lowgular.io](https://lowgular.io/?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/10/Screenshot_2024-10-06_at_22.49.11-1-1.webp) ### Erick Rodriguez > Lead Software Engineer at Doran Jones Inc. I specialize in full stack software development. I like Angular and love to do things with Nx Framework. - [https://x.com/ErickRodrCodes](https://x.com/ErickRodrCodes?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2025/07/my-linkedin-2-1.jpeg) ### Eduard Krivanek > Software developer specializing mainly in Angular and occasional NestJS/Firebase at Multitude IT Labs. Publishing technical articles since September 2022, primarily on Angular, covering interesting challenges, patterns, and learnings I encounter throughout my development journey. - [linkedin/eduard-krivanek](https://www.linkedin.com/in/eduard-krivanek/?ref=angularspace.com) - [**eduardkrivanek.com**](https://eduardkrivanek.com/?ref=angularspace.com) 0:00 /0:09 1× --- ## Q1: If you had to pick only 5 libraries as dependencies for your Angular app, which ones would you pick? ### A1: Kevin Kreuzer The essential library I use for all my projects is prettier. If I use prettier I usually also bring something like Husky or lint-staged that allows me to run prettier on all changed files on every commit. When I implement a UI I usually pick some UI library such as Material or Taiga UI or if I do not need pre-packaged components I often grep Tailwind. ### A2: Rainer Hahnekamp @ngrx/store, date-fns, @playwright/test, @softarc/sheriff-core, angular-eslint ### A3: Alex Inkin Taiga Family is all I need 😄 ### A4: Erick Rodriguez Angular Per see includes incredible features that are out of the box. For example, controlling an HTTP request could be either done by HTTPClient, or if I were to modify the request, I would use an interceptor. Because my job is related to IU, I would like to find more Styled Components to combine with DaisyUI with is a real time-saver. Just that: Angular Styled components with DaisyUI would be my choice. ### A5: Eduard Krivanek Angular Material, Prettier, Ngxtension, Firebase, AnalogJS (if SSR/SSG is needed). --- ## Q2: What’s your go-to strategy for debugging Angular components? ### A1: Kevin Kreuzer When debugging Angular components, I employ a combination of techniques to efficiently identify and resolve issues: - **Console.log:** Yes, console.log. Inserting `console.log()` statements at crucial points in the code helps me inspect variable values and understand the flow of execution. It's a quick way to verify that data is being passed correctly between components and services. - **String Interpolation in Templates** By using string interpolation (e.g., `{{ variable }}`) within the component's template, I can display the values of variables directly in the UI. This method has the advantage of showing whether Angular's change detection (CD) is running correctly, as it updates the displayed values in real-time. - **Angular DevTools for Dependency Injection Errors:** For more complex issues, especially related to dependency injection (DI), I utilize the **Angular DevTools** browser extension. It provides a detailed view of the component tree, services, and DI tokens, allowing me to trace and resolve DI errors effectively. - **Debugger in Browser DevTools with Sourcemaps**: I use the browser's built-in developer tools to set breakpoints and step through the code. Sourcemaps are essential here, as they map the transpiled JavaScript back to the original TypeScript code, making it easier to debug. This method helps in understanding the execution context and identifying logical errors. ### A2: Rainer Hahnekamp I'm using the debugger from the Chrome DevTools. If crunch-time starts, `console.log` as well 😊 ### A3: Alex Inkin `console.log` 😊 ### A4: Erick Rodriguez My favorite way to debug Angular components is based on the following strategy: 1. Use Angular Devtools to inspect the component tree and see the data flow. If where I am I cannot inspect the component tree, I would use VSCode to debug the component. 2. VsCode is particularly useful for debugging Angular Components if what you want is to see how your data is flowing. Personally I HATE adding "debugger" or force a breakpoint on the browser because THE CLIENT will not do that, however VSCode presents to me what the browser has in a more friendly way. From here I can inspect many things, like the data flow, the state of the component, and the state of the data returned by a particular service. ### A5: Eduard Krivanek Initially, I start with console.log, but if I need to debug a more complex issue, such as data flow, I launch the debugger in VS Code. --- ## Q3: Have you ever created a custom RxJS operator? What problem did it solve? ### A1: Kevin Kreuzer Yes, I have created a custom RxJS operator to address a specific need in a project involving a custom dropdown UI component. Inside the Dropdown we wanted to give the user the opportunity to filter results based on keystrokes, but at the same time we wanted to exclude Key up, Key down and other keys which were doing other things in the dropdown, for example key down selects the next element. Therefore we wrote this operator. ```typescript export function onlyAlphaNumericKeys(src: Observable): Observable { return src.pipe(filter((keyboardEvent: KeyboardEvent) => /[a-z0-9-_äöü]/i.test(keyboardEvent.key))); } ``` ### A2: Rainer Hahnekamp Yes, for type predicates, since that didn't always work with the existing operators. Not sure how it is with the latest version of TypeScript though. Error-safe versions of `switchMap`, `concatMap`, etc, and a debug operator that logs out the values. ### A3: Alex Inkin Many times. Most often I used operators that enter/leave `NgZone`. ### A4: Erick Rodriguez I have not had the opportunity to write my own RxJS Operators. While I love it, I have not had the opportunity to do so. However, RxJS presents many operators to play with that makes life easy, for example: - I have used the "debounceTime" operator to prevent a user from making too many requests to the server. - I have also used the "switchMap" operator to cancel the previous request if a new request is made. - I have also used the "catchError" operator to handle errors in the request. So far, I had not had the need to create my own operator, but I am looking forward to doing so. ### A5: Eduard Krivanek It’s rare that you need to create one, since you can solve most problems with the wide variety of operators RxJS provides. But I did create one called `loadingStatus`, which served a similar purpose to `rxResource`. It exposed loading and error states when making an HTTP call. I go into more detail in my article: [Create Custom RxJS Operators](https://www.angularspace.com/create-custom-rxjs-operators/). --- ## Q4: What unconventional challenges did you have while developing/architecting Angular apps? ### A1: Kevin Kreuzer One of the unconventional challenges I encountered was dealing with circular dependencies in a large-scale Angular application. As the application grew, the interconnectedness of modules, components, and services increased, leading to scenarios where two or more modules depended on each other directly or indirectly. Circular dependencies often caused build failures or runtime errors that were difficult to trace. Furthermore the tight coupling between modules made the codebase hard to maintain and understand. ### A2: Rainer Hahnekamp - SSR & asynchronous task issues - Leaking side-effects from unsubscribed Observables - Failing E2E tests with enabled hydration - Odd error messages due to old TypeScript configurations - Crashing Redux DevTools, because of too nested objects - Non-Functioning Change Detection for `window.navigator.online` ### A3: Alex Inkin As I often work on low level UI components there's a ton of browser quirks to deal with, especially with Safari/iOS. I would argue iOS is the only source of frustration at my job. If you use iPhone and any site shows correctly at your phone — remember that the cost was irreversible nerve cell loss suffered by some poor guy like me. ### A4: Erick Rodriguez Dealing with people tends to be one of the critical parts of developing Angular apps, since everyone has their own way to kill fleas. For this, when I join as lead for a team, the first thing I do is to setup guardrails to prevent the team to go monkey coding. Things like Architecture Decision Records (or ADRs) are a good way to prevent the team to go wild: it has rules, why we use such rules and a clear justification on certain patterns. Merge requests are based on such decisions and keeps the team in the same page. Ah, one more thing: I believe Senior Angular developers should be able to understand concepts of Software Architecture, and I find incredible frustrating to find Seniors not understanding that the code they are making is not just for our current project, but it is part of a bigger set of elements that interact with the application. Not having that kind of perspective affects the team and the project in the long run. ### A5: Eduard Krivanek Browser API support can sometimes shoot you in the foot. It once happened to us that an API was supported by all browsers except Safari. We realized it only after deploying to production and had to apply a quick fix. Ironically, Safari added support for the API a week later, so we could have just waited. As for Angular itself, scaling applications is always a challenge. As a developer, you don’t know where the project or business will be in 2–3 years, so designing reusable and maintainable components/services isn't easy. You can unintentionally introduce cyclic dependencies or end up with components that grow to 2000+ lines of code. --- ## Q5: How do you manage API type consistency between frontend, mobile app, and backend? ### A1: Kevin Kreuzer Based on your setup and tech stack there are different approaches, in practice I have seen the following methods, each has advantages and downsides: - **Shared TypeScript Interfaces:** By defining data models and interfaces in a shared package or library (e.g., an NPM package), both the frontend and backend can import and use the same type definitions. This approach ensures that any changes to data structures are immediately reflected across all layers. - **Swagger/OpenAPI Specifications:** Utilizing Swagger to define API contracts allows automatic generation of client SDKs and server stubs. Tools like Swagger Codegen can generate TypeScript clients that the frontend can use, ensuring consistency. - **Contract Testing**: Implementing contract tests verifies that the backend services adhere to the expected API contracts defined by the frontend and mobile applications. This helps catch discrepancies early in the development process. - **tRPC**: Using tRPC provides end-to-end type safety by sharing types directly between the client and server in TypeScript applications. This eliminates the need for manual type definitions and keeps the API contracts in sync. If you want to know more checkout our blogpost here: [https://angularexperts.io/blog/angular-trpc](https://angularexperts.io/blog/angular-trpc?ref=angularspace.com) - **Monorepo Architecture:** Adopting a monorepo setup (e.g., using Nx or Lerna) allows all parts of the application—frontend, backend, and mobile—to reside in a single repository. Shared code and types can be easily managed and imported where needed. ### A2: Rainer Hahnekamp I use OpenAPI as often as I can. If it is very critical, I add an additional layer of Zod for runtime validation of the types. ### A3: Alex Inkin Use DI tokens that detect platform through user agent and then branch CSS and JS depending on it when necessary. ### A4: Erick Rodriguez While desirable to have a common stack where the types are shared across a monorepo, not all the time we will find this scenario. If you are blessed to have OpenAPI, you can generate the types for the frontend and the mobile app from the OpenAPI specs, which is a real time-saver. If you don not have it, but you have swagger, it will be more difficult as swagger might not be as accurate as OpenAPI, and you depend on verbal contracts between your backend team and your frontend team and record those decisions on the respective ticket. If you are bestowed with wisdom, you could use Zod to evaluate the response and ensure it matches the expected response. This way, you can ensure that the response is what you expect, but at the same time, backend wont have any clue of what you are doing. Documented contracts, in this case, works better. Not all the time you will find the toy you want, but you can always play with the toys you have. ### A5: Eduard Krivanek I love when the backend provides types via Swagger or OpenAPI. If you’re using GraphQL, you can generate types as well. In a monorepo, or something like AnalogJS, you can create a shared library for your type system. I've also heard good things about tRPC, though I haven’t used it personally. --- ## Q6: What’s your approach to optimizing the performance of large Angular components? ### A1: Kevin Kreuzer - **Change Detection Strategy OnPush:** Setting the component's change detection strategy to `OnPush` reduces the frequency of change detection cycles. This means Angular only checks for changes when the component's inputs change or when an event occurs within the component, improving performance. - **Defer Loading with defer**: Using the `defer` directive to load heavy or non-critical parts of the template lazily. This defers the loading and initialization until it's necessary. - **Cleanup Subscriptions**: Ensuring that all `Observable` subscriptions are properly unsubscribed when the component is destroyed. This prevents memory leaks and unnecessary processing. - **Use Async Pipe:** Leveraging the `async` pipe in templates automatically handles subscription and unsubscription, reducing boilerplate code and potential memory leaks. - **Using Signals**: Embracing Signals resolves the subscription issue since the subscriptions are automatically managed by RxJs interop like toSignal for example. - **Avoid Expensive Computations in Templates:** Moving complex calculations out of templates and into component code or using memoization to cache results. - **Optimize Loops and DOM Manipulations:** Avoiding deep nested loops or large iterations in the template. Using trackBy functions in `*ngFor` directives to help Angular optimize rendering. With new @for track is mandatory but in past it wasnt. ### A2: Rainer Hahnekamp Don't preoptimize. Before zoneless, I avoided even OnPush. If there is a performance issue, try to locate it. The profiler from the Angular DevTools shows the change detection cycles and how long they run. They are the starting point for me. ### A3: Alex Inkin Don't make it bad in the first place. ### A4: Erick Rodriguez It is a rule of thumb to keep the components as small as possible. If a component is too big, it is a sign that the component is doing too much. For this, I would split the component into smaller components and use the "OnPush" change detection strategy to prevent Angular from checking the component tree every time a change is made. If I had Nx, I could write custom eslint rules that can prevent the component from growing too much. For example, compoents for more than 500 lines? a custom eslint rule will prevent that. BTW, there is an incredible article about atomic composition that every angular developer should read: [https://atomicdesign.bradfrost.com/chapter-2/](https://atomicdesign.bradfrost.com/chapter-2/?ref=angularspace.com) as it resumes the idea of keeping components small and reusable from the perspective of design. ### A5: Eduard Krivanek When I see a slow application, the problems I look for are: - Are we using the `track` key in `@for` loops to optimize rendering? - Are we using pipes instead of function calls in templates? - Are we using pagination or virtual scrolling when rendering large sets of items in the DOM? - If we have code that doesn’t interact with Angular’s APIs (like listening for element clicks or scroll events), are we running it outside the Angular zone? An example: [Simple User Event Tracker In Angular](https://www.angularspace.com/simple-user-event-tracker-in-angular/) - Are we using lazy loading for images, routes, modules, and components (with `defer`)? - Are we unsubscribing from Observables? --- ## Q7: How have you used TypeScript generics to simplify handling complex interfaces in Angular? If so, any examples? ### A1: Kevin Kreuzer Yes, TypeScript generics are a powerful feature that I've used to create flexible and reusable code structures. A simple example would be a generic ApiService: ```typescript export class ApiService { constructor(private http: HttpClient, private url: string) {} getAll(): Observable { return this.http.get(this.url); } getById(id: string): Observable { return this.http.get(`${this.url}/${id}`); } create(item: T): Observable { return this.http.post(this.url, item); } update(id: string, item: T): Observable { return this.http.put(`${this.url}/${id}`, item); } delete(id: string): Observable { return this.http.delete(`${this.url}/${id}`); } } ``` This service can be instantiated with different types: ```typescript const userService = new ApiService(httpClient, '/api/users'); const productService = new ApiService(httpClient, '/api/products'); ``` ### A2: Rainer Hahnekamp I did some proof-of-concepts/drafts for the NgRx Signal Store and am also working on the ngrx-toolkit, which heavily utilizes advanced TypeScript concepts. ### A3: Alex Inkin I always try to work with generics rather than particular data models because my job is reusable flexible components. ### A4: Erick Rodriguez Writting the right type generics will help you not only to keep your code totally reusable, but also easy to understand. For example, your HttpObservable will return something of a expected type. If you are using a service that returns a list of items, you can use generics to define the type of the list, and have those type definitions to be out on the service implementation on the component side of things, as the subscription will resolve automatically your generic type, and you do not need to assert its type. Services should hold absolute control over the type handling on your request/response to data services, as it will help you to keep your code clean and easy to understand. ### A5: Eduard Krivanek When I first heard about TypeScript, I thought it was just about creating interfaces. But when I discovered generics, I was amazed at what’s possible. I now try to use generics whenever I can, deriving subsequent types from existing ones to reduce duplication and improve maintainability. --- ## Q8: Can you use `::ng-deep`? ### A1: Kevin Kreuzer Yes, I can use `::ng-deep` in certain scenarios, but with caution. `::ng-deep` is a pseudo-selector that allows you to apply styles to child components that are encapsulated, effectively penetrating Angular's style encapsulation. ### A2: Rainer Hahnekamp Yes, but I try to avoid them. If I want styles to apply to subcomponents, I usually disable view encapsulation and create rules that apply to the component selector. ### A3: Alex Inkin Yes. ### A4: Erick Rodriguez I CAN, but implementation now a days seems a trade off between Angular Material Components and your needs. If you are using other UI library, it might depend on the implementation you use on it. Personally, I used in extremelly few cases, and I tend to avoid it as much as possible to avoid polluting the design system. ### A5: Eduard Krivanek You can, I use it, but it can backfire. When overriding styles of your own components, you usually know what you're doing. But when overriding styles from an external library, it can cause issues. For example, when Angular Material moved from v14 to v15 and adopted [MDC components](https://v15.material.angular.dev/guide/mdc-migration?ref=angularspace.com), many internal class names changed. If you relied on ::ng-deep to style those, your styles likely broke. --- ## Q9: How do you ensure reusability in your SCSS for large-scale Angular projects? ### A1: Kevin Kreuzer To promote SCSS reusability and maintainability, I employ the following practices: - **Mixins:** Creating SCSS mixins allows me to define reusable style patterns. - **Functions:** Using SCSS functions for calculations or color manipulations. - **Variables:** Defining variables for colors, fonts, spacing, etc. While SCSS variables are useful, I often prefer CSS variables for their runtime flexibility and ability to be manipulated via JavaScript. - **@extend** : Using `%` selectors and `@extend` to share common styles. - **Modular Architecture:** Organizing SCSS files into modules and partials for different components, themes, or features. - **BEM Methodology:** Adopting the Block Element Modifier (BEM) naming convention for classes to enhance readability and reusability. ### A2: Rainer Hahnekamp In general, I have given up the idea of creating global SCSS mixins on my own. I use tailwind, Angular Material, PrimeNG and keep the individual CSS in the component. ### A3: Alex Inkin Angular is great at encapsulating styles so we do not really need to worry about it. ### A4: Erick Rodriguez Globals are a great tool to reuse scss across multiple systems exposed through a library, and it tends to be quite effective when you have a design system in place. Those globals can be overriden in specific apps to not only allow reusing the same global keys, but also to allow the app to have its own identity. ### A5: Eduard Krivanek I’m not a big fan of global reusable styles. Either each component should have its own scoped SCSS to avoid unexpected side effects, or you use Tailwind, which I personally prefer. The only global styles I support are things like color variables. --- ## Q10: Have you contributed to the Angular ecosystem/community? If so, what was your contribution? ### A1: Kevin Kreuzer Yes, I've actively contributed to the Angular community by developing and maintaining several open-source projects: - **svg-to-ts:** A tool that converts SVG files into TypeScript modules. It helps developers include SVGs directly into their Angular applications with type safety and without the overhead of HTTP requests. [GitHub - svg-to-ts](https://github.com/kreuzerk/svg-to-ts?ref=angularspace.com) - **pretty-html-log:** A utility for logging HTML content prettily in the console, making it easier to debug and inspect tests. [GitHub - pretty-html-log](https://github.com/kreuzerk/pretty-html-log?ref=angularspace.com) - **ng-sortgrid**: An Angular library that provides a sortable grid layout, enabling drag-and-drop rearrangement of grid items. [GitHub - ng-sortgrid](https://github.com/kreuzerk/ng-sortgrid?ref=angularspace.com) - **ng-parsel**: A parser library for Angular applications that assists in processing and interpreting complex data formats. [GitHub - ng-parsel](https://github.com/kreuzerk/ng-parsel?ref=angularspace.com) - **nx-release:** A tool designed to streamline the release process in Nx monorepos. It automates versioning, changelog generation, and publishing of packages. [GitHub - nx-release](https://github.com/kreuzerk/nx-release?ref=angularspace.com) And more, in total I am currently maintaining 20+ OS projects. ### A2: Rainer Hahnekamp Yeah, so I contribute mainly via videos on my personal YouTube channel, and I also do ng-news, a weekly newsletter available on multiple platforms. Other then that, I am: - part of the NgRx team as a trusted collaborator - the main developer behind Sheriff, which provides module rules for TypeScript projects. - maintaining the ngrx-toolkit and contributing the Redux Devtools and Redux extensions. - about to start to get more into Native Federation as well, which is a solution for MicroFrontends based on web standards like import maps. ### A3: Alex Inkin I'm a tech writer and an open source maintainer. ### A4: Erick Rodriguez I have been active on Angular Spaces for almost 4 months, and Writting articles has been a way to contribute to the community. Hello to our Discord community in Angular Spaces! ### A5: Eduard Krivanek Not directly to Angular yet, maybe one day. But I’ve been publishing articles since September 2022, aiming for one technical article per month. I write about topics I’m learning or real world problems I’ve faced in my work. --- ## Q11: What is your strategy for refactoring a bloated Angular component? ### A1: Kevin Kreuzer Refactoring a bloated component involves careful planning and execution to improve its maintainability and performance. My strategy includes: - **Identify and Split Responsibilities:** Analyze the component to identify distinct functionalities or responsibilities that can be extracted. - **Create Child Components:** Break down the UI into smaller, reusable child components. - **Extract Services:** Move business logic, data fetching, or state management into services. - **Separate View Logic from Business Logic:** Ensure that the component focuses on presentation logic while business logic resides in services or state management solutions (e.g., NgRx). - **Reusable Templates:** Use structural directives (`*ngIf`, `*ngFor`) and template references to create reusable templates or directives that can be shared across components. - **Simplify Template:** Clean up the template by removing unnecessary bindings or complex expressions. ### A2: Rainer Hahnekamp First, I will make sure that there are good E2E tests available. If I have to make big changes, they are the only ones that keep me and the application safe. Then, I'd start to cut the application into feature and shared modules. Once that is done, I'd go into each feature module and split it into different module types: feature, ui, data, and model. That should give me a good start for further refactoring, which typically depends on the use case. ### A3: Alex Inkin Break it down to small independent pieces first, then rewrite code better. ### A4: Mateusz Łędzewicz A bloated Angular component is a common issue in almost every project I've been a part of or consulted on. Unfortunately, there’s no magic pill that can solve all the problems at once, but there are strategies that can help. Let’s start with some actionable steps. #### Tactical Level: Layers --- I like to think of every project as having three key layers: **UI**, **Application Layer**, and **Data Layer**. If you're not aware of this separation, it’s likely because all these layers are crammed into your component. Here’s the first round of refactoring: - Move all data-related logic to **data services**. - Move all application logic (state, models, etc.) to **application services**. - Keep only the **UI** logic in the component. It should: - Fetch data through the application layer (using queries). - React to events and trigger commands in the application layer. ![Layers](https://cdn.lowgular.academy/images/lesson-description/480uQwJEnnWTJeCObeZj.png) This approach will undoubtedly make your component slimmer, but that’s just the beginning. #### Operational Level: Domain Focus --- Now, let’s zoom out and take a broader view. The strategy mentioned above is a good start, but there’s another common issue in many projects: **multi-domain components**. This problem arises because developers often think of a component as a "page." However, to create reusable and extendable components, we should break them down into smaller, more focused pieces that can be reused across different routes. If we treat components as small, single-purpose blocks of code, we’ll end up with more granular and maintainable components. Of course, there’s more to discuss about how to connect these smaller components, but I don’t want to make this article as "bloated" as the components we're trying to fix! 😄 #### Do I Even Need a Component? --- Here’s another trick for handling bloated components: **Don’t create a component at all**. I know it sounds weird, but as Angular developers, we often default to creating components when it’s not always necessary. In some cases, a **directive** might be a better solution. #### Summary --- The question may seem straightforward, but it touches on several important aspects of development craftsmanship. From separating layers and applying Domain-Driven Design (DDD) principles to splitting features and leveraging the full potential of the framework, there’s a lot to consider. However, the first step is always to keep your layers clean and separate! ### A5: Erick Rodriguez As I have explained before, large components are a total headache, not only for the application to hold it, but also for the dev to read it. Do you want to read 3000 lines of beautiful spagghettified components? I pass, thank you. However if you have no choice but to refactor that mammoth of component, I would always use principles of atomic design to prevent not only to insult myself by writting bloated code (I am a Senior, I should know better), but also to prevent the component to grow too much. That said, small components are EASY to test, larges pieces made of this small components ARE ALSO SMALL, and so on. If you keep atomic composition in place (and discipline yourself in the process) you will note that refactoring mammoths of components will be easier the more you practice such atomic principles. ### A6: Eduard Krivanek You should understand what the project does and be aware of all edge cases. Make sure to have some E2E tests in place to verify the application's core functionality before beginning the refactor. When refactoring, try to split the application modules into smaller, manageable pieces, start with the simplest. Focus first on clean presentational components and services, and then tackle the bloated components. Also, understand that fully refactoring a large app might take a year or more, so it’s important to create a plan with your project manager. --- ## Q12: What was the most complex form you worked with? ### A1: Kevin Kreuzer The most complex form I've worked with was a highly dynamic layout builder which basically allowed you to create any kind of grid layout via a GUI. Here’s a simplified drawing of it: ![Image](https://lh7-rt.googleusercontent.com/docsz/AD_4nXeeYIFQkpwug4uISid9sJfPrFCaBpiai8hAB001bghV2ZzqpESyxT7uldj8oF2E6Ir8FWE7B1WkQQ5lhKcsnEJI6UNWXzYjFo7QaueuM7o7xSSR9xOqbfTUz6GbqTMbkQESX3l3uOfTUN8WdVQPAt-pkvTa?key=rQoaaNBFEpODps0tFk5kAw) The form was complex because it had had the following characteristics: - **Dynamic Fields:** Users could add, remove, and reorder form fields on the fly. - **Conditional Logic:** The visibility and validation of certain fields depended on the values entered in other fields. - **Nested Form Groups:** The form included nested groups and arrays to handle complex data structures like lists and subforms. ### A2: Rainer Hahnekamp It wasn't that big. A wizard containing four different pages. The biggest challenge was to keep the validation rules in sync (frontend & backend). ### A3: Alex Inkin A dynamic settings for a Bitcoin node service that is defined by a spec object coming from backend. ### A4: Erick Rodriguez Dynamic forms tends to be the trade off of everyone that has ever touch Angular projects. In my personal perspective, handling reactive forms tends to be the way to go as you can register new fields on the fly, and remove them as well, also its validation is more programatically than template driven forms. So far, medical application bills tends to be the one of the most complex forms I have ever worked with, as it has a lot of fields, and conditioning responses on certain fields forces you to generate new sets of input data with their respective validations to be made. ### A5: Eduard Krivanek I once worked on an application called GGFinance, which featured an internal trading simulator. You could configure how many rounds the simulator would run, how long each round should take, select stock symbols, define their prices, simulate market crashes, set symbol quantities, and control how often players receive money, something like Monopoly. You can check the video here: [GGFinance - Trading Simulator](https://youtu.be/zSTYdXfrCa0?t=227&ref=angularspace.com). It’s in Slovak, but the entire page shown is one massive form. The [code is public on GitHub](https://github.com/krivanek06/market-monitor/blob/main/libs/modules/trading-simulator/features/src/lib/trading-simulator-form/trading-simulator-form.component.ts?ref=angularspace.com) since the project is no longer active. --- ## Q13: How do you implement validation for in complex forms? ### A1: Rainer Hahnekamp ngx-formly allows advanced validation rules. Then there is also vest, which I still want to try out. Another option is to put the formGroup into a Service or state management. That should be enough for most cases. ### A2: Alex Inkin Mostly through validators on reactive form and sometimes with `NG_VALIDATORS` directives to declaratively apply validation in template. For example, when fields are cross-dependent like "confirm password" ### A3: Erick Rodriguez I always go for complex forms by using a reactive form approach. Common validations can be placed by using directives on the requested inputs to lower the amount of programatical validation on the main form component. For example, if you have a date input, you can use a directive to validate the date, and if the date is not valid, you can prevent the form to be submitted. ### A4: Eduard Krivanek When I use `ControlValueAccessor` for a custom input, I sometimes also implement `NG_VALIDATORS` for validation directly within the component. If I need a custom validator, like checking if a username is already taken during registration, I follow Angular's official documentation for [creating custom validators](https://angular.dev/guide/forms/form-validation?ref=angularspace.com#defining-custom-validators). --- ## Q14: Signals, Observables, Promises - tell me all about differences and when to use which. ### A1: Kevin Kreuzer ![Image](https://lh7-rt.googleusercontent.com/docsz/AD_4nXdUjy2JO-mY6-aed_wndVu04oRaqZhH9qcJi_cstWR1qwyI4Y_YjKQ9jrrTqku-6jkdJiPJFZKw3VTHe72x3tgRD7__6APqqYElhA2pMu4eRp_-By-Ps3R0ajj1w1ez9K2c4rC54KPi90bpbJinRvmq3bTk?key=rQoaaNBFEpODps0tFk5kAw) Promises deliver a single value, therefore they are good for things that just deliver single values such as HTTP requests. Observables deliver one or multiple values either async or sync. Therefore they are good for event based APIs. A great use case for Observables is a stream of clicks. Signals are kinda in between Push and Pull. They also deliver multiple values. They have the possibility to notify you when something changes but they still have to actively be called (either by you or Angular) in order to get the value therefore it’s kinda Push / Pull based. I see that many people are confused about when to use RxJs and when to use Signals. Signals should generally be used whenever it makes sense to ask the question: “What is the current state?”. Therefore HTTP requests will never be Signals since it doesn’t make sense to ask for a current value of an HTTP request. You either get a value or an error, since its only one value Promises would make sense here. Form on the other hands are an ideal candidate for Signals since it makes sense to ask for the current value. ### A2: Rainer Hahnekamp Signals whenever you are in the template. We know that Signal Components are coming and we can prepare for that. Generally speaking, I would try Promises first. As soon as I see it is going to be about managing race conditions, or I need to make use of the powerful pipe operators it is going to be RxJs. So RxJs for the harder stuff and Signals when it is easier, or I am in the template. ### A3: Alex Inkin Signals for all things possible, promises for one time async data, observables for event-like multiple async data processing. ### A4: Erick Rodriguez Signals are a way to handle events in Angular, and has become a daily trade now a days. Signals can be read-only or writable, and you have to determine the best case scenario on how to use it. Observables handles streaming of data, and it is a way to handle data in a more reactive way. Observables can be used to handle data that will be resolved in the future, and it is a way to handle data in a more reactive way. Promises handles one multiple expected results from asynchronous operations that can be used on the future. It is quite useful for the reactivity of your component. ### A5: Eduard Krivanek I use signals for state management and for every property in a component that's used in a template. Signals are great because you can derive new ones based on existing signals. I believe 85–90% of use cases can be solved with signals. The rest is where Observables shine, especially for building declarative, event-driven logic. Promises, are fine for onetime async operations (like HTTP requests), but lack reactivity so I prefer converting them into Observables. --- ## Q15: Have you implemented custom decorators? If so, what was the use case? ### A1: Rainer Hahnekamp No, never. I also never implemented an annotation in Java. Guess, it is not my kind of style. ### A2: Alex Inkin A few times. Most popular was memoization decorator that implemented lazy getter pattern and for functions it worked kind of like pipe, meaning it recalculated method only when arguments changed. ### A3: Erick Rodriguez Nah, so far not yet. I have not had the need to implement custom decorators. ### A4: Eduard Krivanek I once wrote an article on [Deep Dive Into Angular Pipes Implementation](https://dev.to/this-is-angular/deep-dive-into-angular-pipes-implementation-2g5n?ref=angularspace.com) where I created a decorator called `@customMemoize()`. Its purpose was to enable function calls in a template by caching inputs and outputs, essentially mimicking pipe behavior. While this was more of an experimental exploration (and I don’t recommend calling functions in templates), it demonstrated what’s possible. Another useful decorator I created was `@Confirm('message')`, which, when applied to a method, displayed a confirmation dialog. The method executed only after the user confirmed the process. --- ## Q16: What kind of states do we have in the app and how do you manage them? ### A1: Rainer Hahnekamp I differentiate between local state which I don't manage explicitly. Values in a single component for example. Then UI and "entity/server" state. For entity/state I tend to go with the Signal Store. UI state doesn't happen that often because my applications are most of the times forms and grids, but if, then also state management. ### A2: Alex Inkin I mostly work on low level UI, I never really manage global state. But when I do it's basically a behavior subject. ### A3: Erick Rodriguez Traditionally we have a global state which can be used by something like NgRx, and a local state that can be used by the component itself using Subjects. That is how traditionally we manage the state of the application. --- ## Q17: What’s your approach to lazy-loading modules in Angular? ### A1: Rainer Hahnekamp So if I have my feaure/domain groups, I load them lazily. I don't lazily load every component that is attached to a route. ### A2: Alex Inkin Lazy routing mostly, sometimes lazy loaded dynamic components. ### A3: Tomas Trajan Now that Angular provides standalone APIs, I think is start to talk about lazy loading features (views, or sometimes pages, even though a feature can definitely be smaller than a page). Additionally, we should always strive to minimize the amount of ways we’re doing things in our workspace. Let’s take routing as an example, currently there are at least 4 ways to perform routing 1. eager route to a component with component - is eager so doesn't apply directly for our example (even though first component of lazy feature can be referenced as eager component) 2. lazy route to a component with loadComponent 3. lazy route to a module with loadChildren 4. lazy route to **routes based feature** (feature-x.routes.ts) with loadChildren In this case, we want to pick the best one and stick with it. I would personally recommend always define a lazy route as a **routes based feature** which is referenced with loadChildren which is the most modern and flexible way of doing things. Then, in case our lazy feature contains sub navigation, we can lazy load additional components with loadComponent. We should do this also (and especially) in the case if our lazy feature has only one component to start with because the chances are high the requirements will be extended or adjusted in the future. Proposed approach allows us to seamlessly grow to any amount of complexity while maintaining a single unified way of doing this across the whole project which removes cognitive load because everything looks and is done the same way! ```typescript // app.routes.ts export const routes: Routes = [ { path: 'dashboard', loadChildren: () => import('./features/dashboard/dahsboard.routes.ts').then(m => m.routes) } ] // dashboard.routes.ts (routes based lazy feature) export const routes: Routes = [ { path: '', loadComponent: () => import('./dahsboard.component.ts').then(m => m.DashboardComponent)    children: [        // additional routes can come here (always display DashboardComponent + nested routes)    ] }, // or here, replace dashboard component with the dashboard editor in the view (sibling views) // which is easy to extend with in the future, eg { path: 'editor', loadComponent: () => import('./dahsboard-editor.component.ts').then(m => m.DashboardEditorComponent) }, // or even a larger sub-feature { path: 'forecast', // forecast lazy sub feature added later loadChildren: () => import('./forecast/forecast.routes.ts').then(m => m.routes) } ]; ``` ### A4: Erick Rodriguez If you have a big application with too many moving parts, routing your component in a way that allow the use of lazy loading is a must. This way, you can prevent the application to load all the components at once, and only load the components that are needed. This way, you can prevent the application to be slow and only load the components that are needed. However, debugging lazy loaded modules can be a pain as you cannot assert an specific error caused by foreign aspects of the application that are difficult to control. ### A5: Eduard Krivanek I use lazy loading for routes every time. Occasionally, I consider using dynamic components or the `defer` syntax when I have too many moving parts in one component’s template. --- ## Q18: How do you handle HTTP errors globally? ### A1: Rainer Hahnekamp With an HttpInterceptor and have a global ErrorHandler as fallback for non-HTTP errors. ### A2: Alex Inkin By providing ErrorHandler service ### A3: Tomas Trajan In general I would nowadays try to handle errors as close to the part of the UI where they originated as that seems to be the UX best practice all big players are following in recent years.Such outcome could be achieved in many different ways: - component based state management - component store - dedicated state slice of the global store with loading and error states for the entity which can be accessed locally using seelctors But if the goal would be to really handle things globally, e.g. with a global notification or toast,then the best way to achieve this in Angular would be to implement a global HTTP interceptor which will filter HTTP events based on their status codes.Then if the error is detected, and there is some unified format of the errors provided by the API, such interceptor could extract the error metadata and opena locally implemented overlay (or some component like toast, or message from a 3rd party component library) to show this error to the user. Besides that, such an interceptor could also provide fallback values like empty array or entity for the target ui which could then display an empty state, e.g. no entities were found... Another way would be to implement a custom global error handler which is also possible to override in the Angular and filter for specific types of errors and implement similar global notification logic. ### A4: Erick Rodriguez You can always use the combination of Interceptors and a side UI service that helps you to leverage errors when something happens. For example, you can use a side UI service to show a toast message when an error happens, and you can use an interceptor to handle the error and trigger a change on the state of the application to turn on such specific toast. ### A5: Eduard Krivanek I am not the biggest fan of handling errors globally. I can see using Sentry to catch every global error, but you also need to filter them out, because even something like a failed image load is considered an error. I prefer handling errors close to the feature component that fetches the data. Nowadays, you can use the `resource` API, which provides `loading` and `error` states. If loading data fails, you can display an error notification to the user. --- ## The End Thank you to all the experts for their time. As you can see, every expert has a different approach to answering, and we truly appreciate every contribution. We hope you could learn something from it. What do you think about these answers? Do you have any other questions you would add? Let us know in the comments 😎 More answers coming soon! ### 10x Angular Signals Masterclass GIVEAWAY by Kevin Kreuzer URL: https://www.angularspace.com/angular-signals-masterclass-giveaway/ Last updated: 2024-10-02T16:46:16.000Z I promised new giveaway this week and here I deliver! 🚚 _This post is for subscribers only._ ### Angular Space Online Meetup!!!! URL: https://www.angularspace.com/angular-space-online-meetup/ Last updated: 2024-10-02T17:13:55.000Z Silence before the storm!!! This is something that I have planned for a long, long time! With my friend Paweł, we are running Angular Wroclaw Meetup which is in person only. A lot of people asked me if we are going to record it, or stream it. Answer is no. It's on-site only - that's the target. However Angular Space is a prefect platform to run online meetup! ### Angular Space Online Meetup is not going to be an ordinary one. **We will have:** - regular presentation talks live with Q/A panel after each - 1:1 guest interviews - exclusive to Angular Space Registered Members workshops FREE - podcast like live talks with multiple guests - \+ more - You will have a chance to interact with us live in each of these forms. Especially in workshop form. I want to make this meetup unique but still holding traditional meetup values. Im still crafting strategy but I decided to get the word out and create official branding. So here it is! Stay tuned for more info. ### Brand new YouTube where stream is going to happen \- [https://www.youtube.com/watch?v=\_Xt7MwdZtxA](https://www.youtube.com/watch?v=%5FXt7MwdZtxA&ref=angularspace.com) Subscribe :D :D 0:00 /0:10 1× ### 8 months with Angular Space URL: https://www.angularspace.com/8-months-with-angular-space/ Last updated: 2024-09-23T08:30:28.000Z Angular Space is online for 8 months now. I’m quite happy with these numbers Especially the fact that **Membership is $0** **and 3178 Registered Members** And I managed to giveaway **equivalent of $4760** in books & conf tickets! Hard to get a better deal and it’s only one of many membership perks :) \-> Give me 👍 on this post if you are satisfied with what Angular Space is offering at the moment 😄. \-> Give me 👎 on this post if you are not happy. How to do it: - find this block below the email and click on one of these 2 icons ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Screenshot-2024-09-22-at-01.48.51.png) ### Fascinating Dependency Injection URL: https://www.angularspace.com/fascinating-dependency-injection/ Last updated: 2024-09-20T14:14:51.000Z Dependency Injection is a technique that we use every single day as Angular developers. It allows us to reuse things, access native constructs like `HttpClient`, retrieve data from routing, and much more. However, my general feeling has been that Angular developers often underestimate the capabilities of DI. In this article, we will explore some of the more interesting and useful features that sometimes go overlooked. Let's get started! ## What actually is DI? Often, getting an answer to this question is enough to unlock more features that this mechanism has to offer. And, even more often, people slightly misunderstand how it actually works, leaving us with limited capacity when it comes to problems that could actually be solved with ease using DI. One mental mistake that we can often encounter is when developers think of DI as some sort of a "pool", from which we can "retrieve" instances to work with them afterwards. However, DI is not a pool, or, let's say, an abstract dictionary of objects tied to some keys, but rather a strictly hierarchical system in which the same "key" (what we professionally refer to as "`InjectionToken`") can return very different things depending on the context in which it is being requested. How is this being achieved? Well, we are not going **too** deep into the details (there are many more nuances than we can cover here), but we will mention that DI is closely tied to the DOM structure of the application. Yes, you read it right, the "thing" that helps us get service instances is actually closely related to our DOM tree. What does it mean? Well, when our application renders the UI, for each HTML element it renders in the DOM, it also creates a special object, named `ElementInjector`, that is responsible for dependency injection **in the context of** that element and its children. Now, again, without going into too much details, an element injector can be imagined as an object that has a parent injector (the one that has been created for this element's parent), and has a "dictionary" of tokens and their corresponding instances. Consider this: ```typescript @Directive({ selector: '[appSome]', }) export class SomeDirective implements OnInit { private readonly elementRef = inject(ElementRef); ngOnInit() { console.log(this.elementRef.nativeElement); } } ``` Now, if we use this directive twice in the same template... ```html
Text
``` We will get different elements logged in the console, despite having injected the same `ElementRef`! This is because each of these elements has its own element injector, for which Angular automatically provides the `ElementRef` instance. Then, when we use the directive, Angular will ask the element injector for the instance of the token, and it will return different objects for each element. This, of course, reveals to us how the dependency injection mechanism operates. When we ask for a dependency, Angular first looks at the nearest element injector (what we know as the host element - in this case, the element on which the directive is applied), and, if it finds the token, it will return the instance that was previously provided for that token (`ElementRef` in our case). If it does not find the token, however, it will ask its parent element injector, and so on, until it reaches the root injector. After the root injector, it will ask the [platform injector](https://angular.dev/guide/di/hierarchical-dependency-injection?ref=angularspace.com#platform-injector), which is irrelevant to this article. Finally, if the dependency is not also found there, Angular will move to the very top of the hierarchy, where it stumbles upon the `NullInjector`, a word familiar to all Angular developers from the iconic "`NullInjectorError`: No provider for {token}". `NullInjector` is a special injector that is empty and always throws an error when asked for any token. You might notice that this process is similar to how prototypes work in JavaScript. When we ask for a property in any object, it first looks into the object itself, then its prototype, and so on, until it reaches the `Object.prototype`, and then tries accessing its prototype, which is `null`, thus throwing an error. This is beautiful similarity that is useful to remember when we are dealing with DI. > \[!NOTE\] The process of hierarchical lookup of DI tokens can be modified by different options like `Host`, `Optional`, and so on. We will look at them further down this article. So, as we now understand (at least) how DI lookup works, let's now understand how dependencies are created, or, as the more proper term is, provided. ## Providing dependencies Now this is the place where a lot of our DI magic happens. Obviously, following the lookup process, any dependency that want to inject must first be provided somewhere. Some dependencies, like `ElementRef` that we encountered previously, are automatically provided by Angular when the corresponding element and its injector are being created. Some, of course, more well known to us, have to be manually provided. Understanding what options we have when providing dependencies will help us utilize it in a way that minimizes boilerplate code *everywhere* in our application. Let's start with the simplest one: providing a dependency via a class: ```typescript export const appConfig: ApplicationConfig = { providers: [ SomeService, ], }; ``` This is equivalent to the following: ```typescript export const appConfig: ApplicationConfig = { providers: [ {provide: SomeService, useClass: SomeService}, ], }; ``` As this is self-describing, Angular just gives us a shorthand way to simply declare the class we want to provide. Of course, this is nowadays found only when some service is not provided in the root of our application, which is not often (although an absolutely valid case!). Now, we can also provide a value instead of a class that needs to be instantiated. This is useful for sharing some global configurations wile keeping the type-safety. For instance, lots of Angular apps utilize [environments](https://angular.dev/tools/cli/environments?ref=angularspace.com), special files that get replaced with specific values depending on the type of a build we perform (development, staging, production, etc.). A good practice is to create a token (or maybe a class) that has the same type as whatever data we have in the environment files, for instant, if we have an environment like this: ```typescript export const environment = { production: true, apiUrl: 'https://api.example.com', }; ``` We can create a class that reflects this data so we can inject it anywhere: ```typescript export class ApplicationConfig { readonly production: boolean; readonly apiUrl: string; } ``` Finally, we can utilize the `useValue` option to provide the value of the class we created: ```typescript import { environment } from './environments/environment'; export const appConfig: ApplicationConfig = [ {provide: ApplicationConfig, useValue: environment}, ]; ``` And then we can just inject the environment anywhere without the need to reference the environment file: ```typescript @Injectable() export class SomeService { private readonly environment = inject(Environment); private readonly http = inject(HttpClient); getData() { return this.http.get(this.environment.apiUrl + '/data'); } } ``` Now, the next option that is of interest to us is the `useExisting` option. This is rarely used, however it can be useful if we want to limit our developers using a third-party API. For instance, some other Angular service that we do not own might have multiple methods and properties that can do dramatic things like manipulate the DOM structure of our application or maybe register multiple event listeners, affecting the change detection process. However, we might only be interested in some utility methods from the service, and want to restrict unintended access to other, heavier methods. This way, we can create a shell class with the limited functionality we want to expose, and then provide it as is, while actually using the full-blown third-party service under the hood. ```typescript // list only the methods we want type ExposedThirdPartyApi = Pick; export abstract class ShellService implements ExposedThirdPartyApi { abstract someMethod(): void; abstract someOtherMethod(): void; } ``` And then we can just provide the third-party service through this shell service: ```typescript export const appConfig: ApplicationConfig = { providers: [ {provide: ShellService, useExisting: ThirdPartyService}, ], }; ``` And then, we can simply use the shell service to only access the methods that our application configuration allows: ```typescript @Injectable() export class SomeService { private readonly shellService = inject(ShellService); getData() { return this.shellService.someMethod(); } } ``` > \[!NOTE\] These options like `useValue`, `useExisting` and so on can be used anywhere where providers can be defined, like routes or specific components (in the metadata `providers` option), not just in the application config. However, other methods are inaccessible. This approach is really good for making things private while not directly owning the code that defines them, as is the case with third-party APIs. Finally, we arrive at the most important and interesting piece of this puzzle, and that is providing a dependency dynamically, through the `useFactory` option. This option allows us to define a function that will be called when the dependency is requested, and it will return the instance which will be determined programmatically. For instance, a very basic example of this would be to determine which service to use depending on an environment. For example, we might have several different logging services; the one used in local development should just log messages to the console, while the one used in production should log to a third-party service. We can create a factory function that will return the correct service based on the environment: ```typescript import { environment } from './environments/environment'; export const appConfig: ApplicationConfig = { providers: [ { provide: LoggerService, useFactory: () => { if (environment.production) { return new ThirdPartyLoggerService(); } return new ConsoleLoggerService(); }, }, ], }; ``` And then, we can simply inject the logger service anywhere in our application: ```typescript @Injectable() export class SomeService { private readonly logger = inject(LoggerService); getData() { this.logger.log('Some data'); } } ``` > \[!WARNING\] We should be careful to make the different implementation of the same service to have the same methods, for instance, through defining an interface and having both services implement it. Now, as we are familiar with all options, we can further dive into the `useFactory` pattern and discover some scenarios that might even sound crazy to us at first! ### Dynamic dependencies with query parameters?! When writing Angular apps, we are always careful to notice some "static" (for the lack of a better word) things like services and providers, and "dynamic" things like component state, routing data (like query parameters), and so on. Usually, we think of those as two separate worlds - services get provided and configured when the the app starts, and then components get injected with the services and data they need. But what if I told you we can actually provide services dynamically, based on, say, query parameters? Let's review the following scenario. We have an app to show financial transactions, of which we have expenses and incomes. While the two are related, there are some internal concerns of how a service working with an expense should behave differently than one working with an income. Both services, however, have the same interface, and, furthermore, both the expenses and income components have absolutely the same UI. So, for us, it would make sense to have two services, but only one component, but determine which one to use based on a, for instance, query parameter. Let's see this in action. First, we need an interface that both services will strictly implement: ```typescript export abstract class TransactionService { abstract getTransactions(): Observable; abstract addTransaction(transaction: Transaction): void; abstract deleteTransaction(id: number): void; } ``` > \[!NOTE\] We are using an abstract class instead of an interface because interfaces get removed when TypeScript compiles the code, so it cannot be used as a DI token. Abstract classes, on the other hand, are not removed, and can be used as a DI token, and also implemented like an interface, as we are going to do in this example. Now, we can have two services, one for expenses and one for incomes: ```typescript @Injectable() export class ExpensesService implements TransactionService { private readonly http = inject(HttpClient); getTransactions() { return this.http.get('/api/transactions'); } addTransaction(transaction: Transaction) { this.http.post('/api/transactions', transaction); } deleteTransaction(id: number) { this.http.delete(`/api/transactions/${id}`); } } ``` ```typescript @Injectable() export class IncomeService implements TransactionService { private readonly http = inject(HttpClient); getTransactions() { return this.http.get('/api/transactions/income'); } addTransaction(transaction: Transaction) { this.http.post('/api/transactions/income', transaction); } deleteTransaction(id: number) { this.http.delete(`/api/transactions/income/${id}`); } } ``` Now, all of this has been fairly simple, but how do we then provide the correct one for a specific component, and based on a query parameter no less? Turns out, factories can help us here massively. ```typescript @Component({ providers: [ { provide: TransactionService, useFactory: (route: ActivatedRoute) => { switch (route.snapshot.queryParamMap.get('type')) { case 'income': return new IncomeService(); case 'expense': return new ExpensesService(); default: throw new Error('Invalid query parameter'); } }, deps: [ActivatedRoute], } ], }) export class TransactionsComponent { private readonly transactionService = inject(TransactionService); addTransaction(transaction: Transaction) { this.transactionService.addTransaction(transaction); } } ``` As you can see, we are using a factory function that will be called when the component is created. The component then injects the `TransactionService` abstract class, which is guaranteed to have the same interface as the two services we created. So, based on the query parameter we get, we can provide the correct service to the component. Now, this gives us a fantastic level of flexibility here. Now, next, let's tackle an issue that lots of people do not realized is essentially fixable with DI, and that is sharing complex data structures between components. ### Sharing a form instance from parent to child? Imagine we have a large form that we want to split into multiple components. For instance, we might have a registration form that asks for many fields (first name, last name, email, etc.), but also contains a nested form for the address, with multiple other fields (street, city, zip code, etc.). Our template and component get too big, and we think (rightfully so) that it is a good idea to split the registration component into two, one for the form itself, and one for the address form. Now, we have a problem to solve; we want the child component be as lean as possible, and the parent to handle all the logic, including the definition of the form. Let's imagine the parent component first: ```typescript @Component({...}) export class RegistrationComponent { private readonly form = new FormGroup({ firstName: new FormControl(), lastName: new FormControl(), email: new FormControl(), address: new FormGroup({ street: new FormControl(), city: new FormControl(), zipCode: new FormControl(), }), }); onSubmit() { // submit the form } } ``` Now, how do we send the `this.form.controls.address` to the child component? Well, the most basic way we can achieve this is by creating an input: ```typescript @Component({...}) export class AddressComponent { form = input.required(); } ``` This solves the problem in principle, however, there are some issues with this approach. For instance, the `form` in the `AddressComponent` is not strongly typed, and to achieve this, we have to create a separate type definition for the form, moving away from an inferred type, which makes it less maintainable. Also, using an input means introducing all the problems that inputs can have: potential race conditions, added complexity, and so on. So, what can we do instead? We can create a token that will be used to retrieve the form as a whole, which then both components can inject and use: ```typescript export function createRegistrationForm(): FormGroup { return new FormGroup({ firstName: new FormControl(), lastName: new FormControl(), email: new FormControl(), address: new FormGroup({ street: new FormControl(), city: new FormControl(), zipCode: new FormControl(), }), }); } export const AddressForm = new InjectionToken>>('AddressForm'); ``` And then, we can simply inject the token in both components: ```typescript @Component({...}) export class RegistrationComponent { private readonly form = inject(AddressForm); onSubmit() { // submit the form } } ``` ```typescript @Component({...}) export class AddressComponent { form = inject(AddressForm).controls.address; } ``` This is a much better approach, and it is also more flexible. There is also another way to do this. If we are 100% sure that the address form will only ever be used inside the registration component, we can simply define the form in the parent component, and then use hierarchical DI, which we discussed earlier, to inject the parent component into child and access the form: ```typescript @Component({...}) export class AddressComponent { form = inject(RegistrationComponent).form.controls.address; } ``` However, as this is a very specific scenario, this should be used with caution, on a case-by-case basis, as to not confuse anyone wo tries to read the `AddressComponent` code. Now, on to our last example. Here, we will explore the capability of providing global configurations than can, in fact, be overridden by the developer. ### Providing global configuration Imagine we are writing a reusable component, for instance, one that is showing an overlay with a loading indicator. We use this overlay all over our application, and usually, we also want a text saying "Loading..." under the spinning icon. However, in 1-2 cases, we might want a different text. Of course, we can simply create an input with a default value: ```typescript @Component({...}) export class LoadingComponent { text = input('Loading...'); } ``` This is mostly enough. However, if we want the component to be a part of a library that other developers might use, this can become an issue, because other apps might want a slightly different text (or maybe another language). This becomes even more evident if we are using some sort of a monorepo solution like Nx, where we have multiple apps that depend on this same library, but might want a slightly different configuration. So, what can we do about this? The solution is to use DI in couple with a `optional` lookup configuration: ```typescript export const LoadingText = new InjectionToken('LoadingText'); @Component({...}) export class LoadingComponent { text = input(inject(LoadingText, { optional: true }) ?? ''); } ``` Now, any consumer of the `LoadingComponent` can simply provide the text they want to use as default: ```typescript export const appConfig: ApplicationConfig = { providers: [ {provide: LoadingText, useValue: 'Some other loading text...'}, ], }; ``` And this will be used to override the default value of the input. This won't affect the situations where we provide a custom text via the component input: ```html Content ``` Here, we are using the `optional` lookup modifier to ensure Angular does not throw an error if the token was not provided by the developer. Instead, in this case, the component will rely on the provided input value. ## Conclusion Dependency Injection in Angular is, as the title of this article suggests, truly fascinating. In this article, we managed to cover some slightly unconventional scenarios that might be fixed clearly with DI, but we have only touched the tip of the iceberg here. Dependency injection is powerful, but underutilized, and with this article, I hope to help Angular developers understand and use it more effectively, as it can really be a huge time saver in large applications. ## Small promotion ![Modern Angular.jpeg](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Modern-Angular.jpeg) You might have noticed this article is using signal inputs and the `inejct` function only. The recent upheaval in Angular has caused many developers to be confused about what solutions to chose, how to implement them, and how to migrate their existing codebases to the most recent features. Thankfully, I have a response to this concern: very soon, my very first book is going into print! It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular?ref=angularspace.com). The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on [Twitter](https://twitter.com/Armandotrue?ref=angularspace.com) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/?ref=angularspace.com), where I will be posting whenever there are news or promotions available. P.S. Hey! Check out chapter 3 of my book to uncover more in-depth insights about dependency injection ;) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Screenshot-2024-09-20-at-16.10.33.png) ### Configure your Nx Monorepo to commit code properly under JIRA Standards with Nx, Commitizen and Husky URL: https://www.angularspace.com/configure-your-nx-monorepo-to-commit-code-properly-under-jira-standards-with-nx-commitizen-and-husky/ Last updated: 2024-09-17T13:26:44.000Z I decided that for this article, I will adopt a darker tone, different to my traditional style of writing articles. I hope you enjoy the read, learn something new, and throw a couple chuckles with the audience. ## Introduction In the last five years, it has become almost a trend to require commits to follow a specific format—because who doesn’t love adding extra steps to their workflow? This kind of practice helps programmers in a monorepo understand the project’s history and, of course, tie everything neatly to those ever-so-essential JIRA tickets. Yet, despite all this, I’ve noticed that some incredibly important projects, which are already complicated enough, somehow manage to skip basic features like automatic commit formatting and testing to prevent our CI to die in the most spectacular fashion. And let’s not even talk about the lack of control over what gets pushed to the repository, as if the consequences of messy code are just a minor detail. In short, there are more landmines hidden in code that hasn’t been properly linted, checked, or rigorously tested than you might think — each one just waiting to blow up your CI pipeline and drive up operational costs. Remember, computing power isn’t free, and every time we let CI run wild with bad code, it’s money down the drain. The more we let things slip, the less we get paid. Controlling these issues at the source isn’t just smart — it’s survival. ## Motivation and goals of this article So, in the spirit of saving the world one commit at a time, I decided to write this as a motivation to guide our community through a few simple steps to set up a project so that: 1. Commits are formatted in a clear and consistent way. 2. They meet JIRA's high standards. 3. The code is checked, and the commit is stopped if there are errors. 4. Once the code is clean, additional tasks like running tests or builds are done locally, and if everything passes, the commit is approved for the repository. - Note #1: This article is more of a conceptual guide. Most of the code presented here is a representation of the final setup that will enable your repo to automatically format commits and execute tasks using Nx through git hooks. - Note #2: Working on your personal pet project? [Good news — you can get Jira/Bitbucket integrations even on the free plan!](https://atlassian.com/software/jira/free?ref=angularspace.com) (Seriously, it’s not much different from integrating with GitHub.) - Note #3: Some knowledge of Nx Framework is needed to understand some of the caveats of this article. However I am touching things behind scenes so you understand how Husky, Commitizen and Nx does certain things. ## Requirements For this to **actually** work, forget about any UI commit tool. We’re going to rely on a terminal, git and Nx to do things the right way. Tools like Sourcetree or the built-in Git integration in editors like VSCode and its competitors simply aren’t equipped to handle the level of power you're about to unleash. Use your UI tools to track and visualize your repo if you must, but when it comes to committing code and pushing it to your remotes, leave them behind as Nx combined with commitizen and husky will handle the commits through the terminal in the right way. ## The Terminal For my Mac and Linux friends, please skip this rant—you’re already in good hands. Your native terminals are blazing fast, leaving no room for lag or issues, and Linux commands are your daily bread and butter. This one’s for the folks stuck using Windows and Git Bash. Once you install git on your Windows device it comes with Git Bash. But let's be honest—it’s a half-hearted fix for the terrible performance it delivers. I’m not exaggerating; it can be painfully slow at times. Instead, I’d recommend using something like [oh-my-posh for PowerShell](https://ohmyposh.dev/?ref=angularspace.com). Not only does it let you see your current branch and other neat little details, but the real advantage is that it’s way faster than Git Bash. The trade-off? You’ll need to learn a bit of PowerShell to navigate your file system and do required stuff, but it’s worth the speed boost. If you're feeling adventurous and ready for a challenge, you can still venture into the ageless CMD terminal, armed with nothing but a Coleman lamp in the middle of a dark night. ## And lint-staged? Why aren’t we using it? Yes, it's true, everyone and their grandmother on the internet swears by mixing lint-staged with Husky. Sure, you could use `lint-staged` and hope for the best, because who doesn’t love a little game of chance with their codebase? It’ll catch some issues depending on the file format. Something new pops up? Just slap on another rule and hope it sticks. But that’s like patching up a sinking ship with Flex Tape — Sure, it might hold for a while, but it’s far from a real solution. Instead, using commands like `nx format` or `nx affected -t format` is like reinforcing the entire structure. These tools integrate seamlessly with your monorepo, ensuring that every file is consistently formatted according to your project’s standards, addressing the root of the issue rather than just covering it up. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/tenor.gif) Not under my watch in an Nx Monorepo Do I need to reinvent the wheel? No, but if I can put a tire on it to make it run faster without any hassle, you bet I will. ## Assumptions In our imaginary Nx monorepo, we have Nx 19 with a traditional monorepo approach, with Angular 18, having a main app called `frontend`, and a shared library called `ui`. The git repo is using `git flow` to segregate development and release accordingly. ## First steps Nx is a powerful tool for managing a monorepo, so powerful that some people naively think Nx **is** a monorepo itself. Nx is more than just a machine for generating boilerplate code for your project. Depending on how deep you’re willing to go, you can mostly run the same commands across various types of projects using executors. And those executors? They come with their own packages and runtimes, letting your project do all the essential tasks like: - Build - Run - Test - Lint All common in any framework and language of your choice—as long as Nx graces you with its support via the [Nx Plugins Registry](https://nx.dev/plugin-registry?ref=angularspace.com), which includes a surprisingly extensive list of official and community-made plugins. [Thanks to the magic of Nx Crystal (a new approach from the Nx team to make things more transparent and less bulky in the project.json), Nx can automatically infer task configurations without adding more parameters to the project.json files.](https://nx.dev/concepts/inferred-tasks?ref=angularspace.com) We’re going to take full advantage of this to make testing and linting a more seamless experience, especially since tests can be `cached`, saving us loads of time when things are good to go. With that in mind, let’s prepare the codebase. ## Preparing for plumbing I won't dive too deeply into how Jira can either make us incredibly successful developers — or leave us utterly stressed out. But here’s the gist: each project in Jira has a Code, and most of the time, every request your client has for the software you're building becomes a ticket. A Ticket is a serialized item within your project, formatted like this: `[project code]-[consecutive number]` Let's say we have a project with the code `SOP`, and one of our tickets on the board involves getting Nx to work with Commitizen and Husky. We need to be assigned to this task. If the task has the code `0001`, then the ticket ID would be `SOP-0001`, with a descriptive title on the ticket such as `Goat like feature`, which includes all the needed "documentation" and requested goals of such task. Jira often integrates with Bitbucket or GitHub to create the necessary feature branch from develop. My strong advice? Don’t create the branch from your terminal first. Let Jira handle that for you through the Create Branch option. Trust me, it’s better this way. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image.png) Generating a branch from JIRA will definitively lower the chances for you to screw things up. Once done, you can pull the branch from *remote*: 1. `git fetch origin` 2. `git pull` 3. `git checkout feature/SOP-0001-goat-like-feature` Now that we have pulled and changed our feature branch to `feature/SOP-0001-goat-like-feature`, we need to start to work on the plumbing of our commit process. ## Installing dependencies There are 3 dependencies we will need to install in your repo to ensure the workflow runs properly: - [commitizen](https://www.npmjs.com/package/commitizen?ref=angularspace.com) - [@digitalroute/cz-conventional-changelog-for-jira](https://www.npmjs.com/package/@digitalroute/cz-conventional-changelog-for-jira?ref=angularspace.com) - [husky](https://typicode.github.io/husky?ref=angularspace.com) For the package manager, we will be using npm as the jack of all trades. ```bash npm i -D commitizen @digitalroute/cz-conventional-changelog-for-jira husky ``` Once we have installed the dependencies, we need to start to prepare for things we might need. ## Initialize Husky and configuring it. Husky MUST need to be initialized first. for this we run ```bash npx husky init ``` That will generate the required .husky folder with the needed git hooks to use. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-1.png) Generated .husky folder with the pre-commit hook We’re going to take control of Husky now by using `nx affected` the right way. ## Husky and Nx Commands `nx affected` leverages git history to identify what’s changed compared to the current commit. That’s what makes it so powerful and why I’ve ditched `lint-staged`—because `nx affected` will automatically format the affected files without breaking a sweat. Assuming our development branch is the base branch and holds the last clean commit, we’ll use it as the reference point for detecting affected changes: ```bash npx nx affected -t lint format test --base=origin/develop --parallel=5 ``` One command to do three critical things: - Ensure that affected files are fully linted with all the rules in play for their respective projects. - Make sure that tests for affected projects are running without a hitch. - Automatically format the affected files. All of this happens in parallel with up to 5 threads! I warned you we’d push this, but depending on your machine, 5 threads might be overkill, so use it wisely and adjust to your own device needs. Depending on how your Nx Repo is originally configured, by default, build and test are cacheable items. If you’ve made minimal changes in the `ui` library but none in the `frontend` app, this command will only `lint`, `format`, and `test` the `ui`—because the previous commit from `origin/develop` for `frontend` remains unchanged, and the `test` for `frontend` will just pull the same snapshot result. ### The dangers lurking on caching **A word of caution for those who like to live dangerously and ignore good advice**: I strongly recommend not caching `lint` and `format`, because that comes with a host of problems: - **Stale Results**: Caching lint and format checks might seem clever, but it’s a ticking time bomb. Every time you make a code change, you’re risking relying on outdated information. As your codebase evolves, those old, cached results won’t catch new issues, leaving you blind to potential disasters. - **Inconsistent Code Quality**: Caching can let subpar code slip through the cracks. Code that doesn’t meet current linting or formatting standards might get a free pass, leading to a patchwork of inconsistent quality across your codebase—like letting some parts stay stuck in the past while others move forward. - **Performance Issues**: Caching might look like a shortcut to better performance, but it’s a trap. Over time, it can actually slow down your CI checks as they start running on more files than necessary. Instead of being efficient, you end up dragging the whole process down, like carrying extra baggage you don’t need. - **Breaking Monorepo Principles**: Monorepo development is all about rebuilding, retesting, and relinting only what’s truly affected by changes. Caching throws a wrench into this by giving you a false sense of security, ignoring the current state of the codebase, and undermining the very principles that keep a monorepo running smoothly. Now that you understand why caching `lint` and `format` is a bad idea, let's see what `husky` can do for us along with Nx. ### Command approaches for big and small projects Open the `pre-commit` file in the `.husky` folder and paste this command: ``` nx affected -t lint format test --base=origin/develop --parallel=5 ``` Now, this approach might work for small projects, but larger ones will definitely struggle when it comes to testing. What if the affected project only has ONE component that needs testing? Do I really need to drag the entire test suite through the mud for that? You’ve got two options here: **Option 1: Leave the test to run only affected code**: For this, you’ll need two commands: ```bash nx affected -t test --onlyAffected --base=origin/develop --parallel=5 nx affected -t lint format --base=origin/develop --parallel=5 ``` The key here is the `--onlyAffected flag`. When you use the `--onlyAffected` flag with the `nx affected -t test` command, Nx will narrow down the execution to only those projects directly impacted by your changes. This means it will not just consider projects dependent on the changed files but will also ensure that only the tests for the truly affected projects are run. or **Option 2: Leave the testing to the CI**: You could simply lint and format, and let the CI to be at the mercy of your tests. But remember, testing is a responsibility that demands careful handling. Neglect it at your own peril. Adjust as needed and save it. Now, the next part of this operation will focus on configuring our commits with Commitizen and its cz-conventional-changelog-for-jira plugin. ## Commitizen and cz-conventional-changelog-for-jira This duo is probably the best thing you can have to format your commits, as Commitizen alone lacks the necessary options to embed the Jira Ticket into the message for us. First, in your `package.json`, create a script called `commit` that will handle the heavy lifting: ```json { "scripts":{ "commit" : "git add --all && git-cz" } } ``` Pretty self-explanatory: add all the changes and run Commitizen. For cz-conventional-changelog-for-jira, you have two options: either bloat your package.json a bit with an additional `config` key, or generate a `.czrc` file to add an additional config file to your repo. If you choose to bloat your package.json, you need to add the following: ```diff { "scripts": { "commit": "git add --all && git-cz" }, + "config": { + "commitizen": { + "path": "./node_modules/@digitalroute/cz-conventional-changelog-for-jira" + } + } } ``` Or, if you opted to add a `.czrc` file: ```json { "path": "./node_modules/@digitalroute/cz-conventional-changelog-for-jira" } ``` Now, running `npm run commit` should produce something like this: ![](https://raw.githubusercontent.com/digitalroute/cz-conventional-changelog-for-jira/master/images/demo.gif) The expected result But don’t start to celebrate too early—we’re far from done. It’s quite possible that your CI and integration team has set up some nasty formatting requirements for commit messages. For example, based on the demo above, your message might look like this: ``` SOP-0001 feat: Adds a cool feature ``` But what if your CI team insists on a slight change: ``` SOP-0001. feat: Adds a cool feature ``` Did you see that (dot)? That simple DOT could throw your entire commit into instantaneous chaos, with the CI team laughing in your face (trust me, I’ve been there). ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-2a.png) Depending on your CI team, errors messages can be diverse, but I had clowns on mine it and I got this Yes, right in the gut, and in your ego. You might want to take a moment to learn how to amend your commit with the right format before making the commit to get in worse shape. So, to prevent this distopic nightmare, we will assume you added the `.czrc` file to your code repository. Fortunately, cz-conventional-changelog-for-jira contains a wide variety of options to format our messages properly. [If you were thoughtful and read the whole page for cz-conventional-changelog-for-jira instead of scrolling this article without a significative purpose](https://www.npmjs.com/package/@digitalroute/cz-conventional-changelog-for-jira?ref=angularspace.com), you would see formatting options that can help us to format the message as needed. The needed option is `jiraAppend` and the value represented will be the damn dot: ```diff { - "path": "./node_modules/@digitalroute/cz-conventional-changelog-for-jira" + "path": "./node_modules/@digitalroute/cz-conventional-changelog-for-jira", + "jiraAppend": "." } ``` Now, next time that you run `npm run commit` you will see the message to be in compliance with the clowns of CI once you push your code to remote. Now you can laugh back at them. We can go now step by step on it and ensure things goes the right way: Run the `npm run commit` will welcome me to the commitizen interface ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-2.png) After picking `feat: a new feature`, it will greet me with the JIRA Issue. you might see grayed down our Jira Ticket `SOP-0001` (Note that it has the dot append as we configured in the .czrc file), which by hitting enter will become by default the ticket to be used. Of course you are free to input a different ticket, but you won't want to have the CI clowns after you blocking your code to do the work of other dev. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-3.png) now include a description accordingly to your needs ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-4.png) if an additional description is needed to clarify your work, you can add it in this prompt, otherwise press enter. The final prompt would be if you are including breaking changes. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-5.png) once you hit enter, you will have a preview of your message (just what everyone reading this article wanted) ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-6.png) You can either cancel the commit and do more changes, or press enter to confirm the commit message. If you decide to commit, from here every affected project will run the required targets (`lint`, `format` and `test`). From here, if something fails, you might need to fix it ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-7.png) ## DO 👏 NOT 👏 STOP 👏 HUSKY 👏 Know this: if the `nx affected` command fails, your code will be reverted to the state it was in before your commit preparation. In large commits, DO NOT ATTEMPT TO PRESS CTRL + C, as your changes are in a fragile middle stage. Behind scenes, Husky uses `git stash` to move your commit code to a rollback state if something goes wrong with `nx affected`. **If you deliberately interrupt the process** (for example, because you spotted a life-altering typo like "featre" instead of "feature" and suddenly it feels like the world is ending), **SOMETIMES** Husky will do more than just ignore your changes—it might toss all your hard work into the twilight zone, turning your precious hours of coding into a nightmare to recover. The first time it happened to me, my soul left my body without a trace. But hey, if you enjoy living on the edge, there’s always the stash created by Husky to pull you back from the brink. **LET HUSKY FINISH**. YOU HAVE BEEN DEARLY WARNED. ## Success!!! Nothing beats the satisfaction of conquering an agonizing Jira ticket, with everything running like clockwork, leading to a successful commit and flawless execution of all `nx affected` commands, culminating in this glorious screen: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/image-8.png) Now, push and create a MR and everyone in the team should be on the same page ## Conclusion So, I hope you’ve enjoyed this article. I’m open to your comments—and, of course, your horror stories about wrangling your team of developers into some semblance of a framework to keep things running smoothly. Stay tuned for the next article—it might just be another untellable nightmare about software and architecture development, or perhaps a shiny piece on Angular's latest toys. Until I wake up from the next one, may your commits be clean and your CI pipeline merciful. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Screenshot-2024-09-17-at-15.24.51.png) ### Bringing Polymorphic Functional Components to Angular with signal inputs URL: https://www.angularspace.com/bringing-polymorphic-functional-components-to-angular-with-signal-inputs-2/ Last updated: 2024-09-12T13:40:37.000Z ## A couple of words on polymorphism As developers strive to make their code more flexible, maintainable, and scalable, they often encounter the concept of polymorphism. In Angular, polymorphism can be applied to views, enabling the same template to dynamically adapt its structure and behavior based on different conditions. To understand what polymorphism looks like in templates, it’s helpful to start by recognizing what it is not. If introducing new requirements to your view results in any of the following scenarios, it's a sign that your code might not be truly polymorphic: 1. Adding New Property Flags: You find yourself adding property flags like `shouldDisplayTooltip`, `isCardVisible`, or `isCollapsible` to control various aspects of the view: ```html @if(isCardVisible) { @if(isHeaderVisible) { Title } Card content } ``` 1. Relying Heavily on `if` or `switch` Statements: Your view depends heavily on conditional logic to display different elements: ```html @switch(true) { @case(viewMode === 'list') { } @case(viewMode === 'grid') { } @case(viewMode === 'detail') { } } ``` 1. Hardcoding Multiple Versions of a View: You’re creating multiple versions of the same view to handle different scenarios: ```html Title @if(isCollapsible) { Collapsible content > } @else { Non-collapsible content } ``` The main difference between polymorphic and non-polymorphic views is that changes to polymorphic views don’t require alterations to the core implementation of the view. This makes the templates more flexible and easier to maintain and expand over time. In this article, we'll explore what polymorphic views are, how they differ from the traditional (imperative) way of creating views. We’ll also explore how recent Angular features have breathed new life into this approach and what fantastic opportunities these features offer us. ## Dynamic views in Angular As previously mentioned, polymorphism in templates occurs when a view dynamically adapts its structure and behavior based on different conditions. Although dynamic views and polymorphic views are not the same, dynamic rendering in templates facilitates the use of polymorphism. Angular provides several built-in mechanisms to support this, including interpolation, the `ngTemplateOutlet` directive for templates, and the `ngComponentOutlet` directive for components. In this article, we will focus primarily on the latter. ### NgComponentOutlet The `ngComponentOutlet` directive was initially introduced in Angular 4\. It enables dynamic rendering of components in Angular templates by allowing a developer to specify a component type at runtime. With Angular's Ivy engine, creating dynamic components has become much simpler. Gone are the days of needing `ComponentFactoryResolver`, `entryComponents`, adding lazy-loading modules to angular.json files, and dealing with the famous injector [issue](https://github.com/angular/angular/issues/11388?ref=angularspace.com) while lazy-loading modules. Ivy allows developers to render components without these cumbersome requirements dynamically. The `ngComponentOutlet` directive now supports on-the-fly component creation, making the process more efficient and straightforward. ## Communicating with dynamic components Rendering a component dynamically is just one piece of the puzzle; the real challenge often lies in efficiently passing input data to these components and handling their output events, all while maintaining strict type safety. ### Angular's Built-in Tools To partially address these challenges, Angular 14 introduced two key features that simplify working with dynamic components: 1. `setInput` method on `ComponentRef` Class: This method provides a more streamlined way to set inputs on dynamically created components. It works seamlessly with both traditional inputs defined using the `@Input` decorator and the newer signal inputs. This method ensures that the inputs are correctly bound to the component, triggering Angular's change detection automatically: ```ts @Component({ ... }) export class MyComponent { private readonly vcr = inject(ViewContainerRef); private createComponent(): void { const componentRef = this.vcr.createComponent( MyDynamicComponent ); componentRef.setInput('name', 'Bob'); } } ``` 1. `ngComponentOutletInputs` Input Property on `NgComponentOutlet` directive: This property allows you to pass an object containing input values directly to a dynamically rendered component via the `NgComponentOutlet` directive. The inputs provided this way are properly bound to the component instance, and change detection is automatically managed, ensuring that components using the `OnPush` strategy are marked for check when necessary: ```ts @Component({ ... }) export class MyComponent { component = MyDynamicComponent; inputs = { data: 'Dynamic Data' }; } ``` ```html ``` Both of these approaches greatly simplify the process of working with dynamic components, especially in terms of managing change detection and lifecycle hooks. However, they do not inherently provide strict type safety. Developers still need to manually ensure that the correct types are being passed to these inputs, as Angular does not enforce this at compile time. This leaves room for potential errors. The situation is more straightforward with outputs, as we can simply access them through the component instance, subscribe to them, and maintain correct typing: ```ts const componentRef = this.vcr.createComponent(MyDynamicComponent); componentRef.instance.onEdit.subscribe((value) => { // Handle an emitted event }); ``` ### Using Dependency Injection Tokens Another approach for passing data involves using dependency injection (DI) tokens, a technique exemplified by the [ng-polymorpheus](https://github.com/taiga-family/ng-polymorpheus?ref=angularspace.com) library. This method partially addresses typing issues by allowing you to define an interface for the expected input data. Here's how it works: ```ts interface MyDynamicComponentContext { name: string; onEdit: (value: string) => void; } @Component({ ... }) export class MyDynamicComponent { private readonly context = inject(POLYMORPHEUS_CONTEXT); } ``` Next, you render the component in your template with the dedicated `polymorpheusOutlet` directive, passing the required context: ```ts @Component({ ... }) export class MyComponent { public readonly component = new PolymorpheusComponent(MyDynamicComponent); public readonly context: MyDynamicComponentContext = { name: 'Bob', onEdit: (value: string) => this.onEdit(value) }; private onEdit(value: string): void { ... } } ``` ```html ``` The `PolymorpheusComponent` class serves as a wrapper around your actual component, allowing for dynamic rendering with context injection: ```ts export class PolymorpheusComponent { constructor(public readonly component: Type, private readonly i?: Injector) {} public createInjector(injector: Injector, useValue?: C): Injector { return Injector.create({ parent: this.i || injector, providers: [ { provide: POLYMORPHEUS_CONTEXT, useValue, }, ], }); } } ``` While this approach provides a powerful way to pass context data, it can reduce component flexibility because the components are specifically designed to be rendered using this method. Consequently, these components may not be as easily reusable with standard input and output properties. Although it is possible to duplicate properties to support both scenarios, doing so introduces additional complexity. Moreover, this approach does not enforce type safety when passing contex. ## Signal inputs and outputs - the missing part of the puzzle Looking back at everything we've discussed so far in this article, it's clear that Angular historically lacked an automatic method for identifying and inferring the types of component inputs. Unlike frameworks like React, where the functional component architecture naturally incorporates the arguments as inputs—making prop type inference straightforward—Angular faced challenges in this area. Traditionally, while Angular could manage output identification via `EventEmitter`, distinguishing input properties from other public properties within components wasn’t automatically feasible. This limitation has been significantly addressed with the introduction of signal inputs. By utilizing the `input` function to create these signal inputs, they are explicitly typed as `InputSignal`, making it possible to identify them and infer their types: ```ts export type ExtractInputSignalsValues = OmitNever<{ [Key in keyof T]: T[Key] extends InputSignal ? ValueType : never; }>; export type PolymorphicComponentInputs> = ExtractInputSignalsValues>; ``` Similarly, for outputs, Angular has enhanced its functionality with the introduction of the `output` function. Outputs created this way are of the `OutputEmitterRef` type: ```ts export type ExtractOutputEmitterRefs = OmitNever<{ [Key in keyof T]: T[Key] extends OutputEmitterRef ? OutputEmitterRef : never; }>; ``` Now, we can easily create a wrapper around our component classes to encapsulate the actual component and facilitate the pre-passing of the component's inputs and outputs: ```ts export class PolymorphicComponent = Type> { public readonly inputs: ValueOrNever>; public readonly outputsHandlers: ValueOrNever>>; constructor(public readonly component: TComponent, private readonly params: PolymorphicComponentParams) { this.inputs = getInputsFromParams(this.params); this.outputsHandlers = getOutputHandlersFromParams(this.params); } } ``` When the component is rendered in a template with a dedicated directive, the necessary inputs and outputs are correctly propagated to the encapsulated component. ## Enough theory, let’s see this in action To create a polymorphic component, we are instantiating the `PolymorphicComponent` class and pass a component class to it with inputs and outputs: ```ts @Component({ selector: 'my-icon', ... }) export class IconComponent { public readonly icon = input(); public readonly iconClicked = output(); } const iconComponent = new PolymorphicComponent( IconComponent, { inputs: { icon: 'search' }, outputsHandlers: { iconClicked: () => { console.log('Clicked') } } } ); ``` To render a polymorphic component in a template, you need to use the `polymorphicComponentOutlet` directive and pass the component to it: ```ts export class MyComponent { public readonly iconComponent = iconComponent; } ``` ```html ``` In cases where you want to override previously provided inputs or add additional output handlers directly via the template, or when inputs are intended to be passed through the template rather than upfront, you can utilize the `polymorphicComponentOutletInputs` and `polymorphicComponentOutletOutputsHandlers` inputs defined on the `polymorphicComponentOutlet` directive: ```ts @Directive({ standalone: true, selector: '[polymorphicComponentOutlet]', }) export class PolymorphicComponentOutletDirective> { public readonly polymorphicComponent = input.required>(); public readonly polymorphicComponentOutletInputs = input>>(); public readonly polymorphicComponentOutletOutputsHandlers = input>>(); ``` Now, we can ensure correct typing at compilation time and prevent potential issues during execution. This allows for a dynamic and flexible way to manage component data and interactions directly from the template: ```html ``` To enhance autocomplete functionality while defining inputs and outputs for components, we can utilize the `createInputsFor` and `createOutputsHandlersFor` functions. These functions ensure that input and output handlers are correctly typed based on the component passed as the first argument, thereby providing accurate autocomplete suggestions: ```ts @Component({ ... imports: [PolymorphicComponentOutletDirective], }) export class MyComponent { public readonly iconComponent = iconComponent; public readonly overriddenInputs = createInputsFor(IconComponent)({ icon: 'delete', }); public readonly additionalOutputHandlers = createOutputsHandlersFor(IconComponent)({ iconClicked: () => { console.log('Handle the click here as well'); }, }); } ``` By distinguishing input signals from other component properties, we can create interfaces that our components implement. This approach enables us to decouple our code from specific component classes and rely on an interface instead, which is the essence of polymorphism. It allows us to handle inputs and outputs without needing to know the exact class being used: ```ts interface IconComponent { icon: InputSignal; iconClicked: OutputEmitterRef; } @Component({ ... }) export class MyIconComponent implements IconComponent { public readonly icon = input(); public readonly iconClicked = output(); } ``` You can use a `type` helper function to easily pass type information as a parameter: ```ts export const type = (): T => ({} as T); @Component({ ... }) export class MyComponent { public readonly iconComponent: Type; public readonly overriddenInputs = createInputsFor(type>())({ icon: 'delete', }); public readonly additionalOutputHandlers = createOutputsHandlersFor(type>())({ iconClicked: () => { console.log('Handle the click here as well'); }, }); } ``` The key point to remember is that components can now be rendered traditionally or via the `polymorphicComponentOutlet` directive, while still allowing for input handling, output management, and ensuring strict type safety: ```html ``` Thus, we finally have a way to ensure strict type safety while working with dynamic components' inputs and outputs. ### Providing async values for inputs Observables or signals can be passed as input values. Any emitted changes will be propagated accordingly, and the change detection process will be triggered: ```ts const icon$ = of('value as observable'); const iconComponent = new PolymorphicComponent(IconComponent, { inputs: { icon: icon$, }, }); const $icon = signal('value as signal'); const iconComponent = new PolymorphicComponent(IconComponent, { inputs: { icon: $icon, }, }); ``` With the introduction of signals in Angular, it's advantageous when a solution supports both observables and signals, handling the transformations seamlessly under the hood. This dual compatibility ensures that dynamic components can interact with data streams efficiently, regardless of the source type, and enhances the adaptability of the code to different reactive programming scenarios. ### Bringing Functional Components to Angular To efficiently generate multiple instances of the same component with varied settings, using the `createPolymorphicComponent` function is ideal due to its support for currying: ```ts export const createPolymorphicComponent = >( component: TComponent ): PolymorphicComponentFactory => { return (params?: PolymorphicComponentParams) => { return new PolymorphicComponent(component, (params ?? {}) as PolymorphicComponentParams); }; }; ``` Here’s how it works: ```ts const createIconComponent = createPolymorphicComponent(IconComponent); const iconOne = createIconComponent({ inputs: { icon: 'search', }, }); const iconTwo = createIconComponent({ inputs: { icon: 'info', }, }); ``` By defining a curried function `createIconComponent`, you can easily configure multiple instances of `IconComponent` with varying inputs. While Angular does not natively support functional components in the traditional sense seen in frameworks like React, the ability to automatically infer input types has opened the door for creating custom adapters like `createPolymorphicComponent`. This tool allows us to employ a functional programming style by managing and instantiating components through function factories, thus enhancing both the flexibility and reusability of Angular components. For instance, the `partial` function can be used to decompose the process of passing inputs into multiple phases: ```ts import { createPolymorphicComponent, partial } from '@shared/util-polymorphic-content'; @Component({ ... }) export class IconComponent { public readonly icon = input(); public readonly direction = input(); public readonly color = input(); } const createIconComponent = createPolymorphicComponent(IconComponent); const createIconComponentPartial = partial(createIconComponent); const createSearchIcon = createIconComponentPartial({ inputs: { icon: 'search', }, }); const searchIcon = createSearchIcon({ inputs: { direction: 'before', color: 'accent', }, }); ``` ### Polymorphic Views Composition The essence of polymorphism lies in its ability to offer highly flexible composition and customization. Let's delve into how this can be implemented by starting with the definition of a `PolymorphicContent` type. This type is designed to handle various forms of content—whether it's a component, template, string, or any other primitive: ```ts export type TemplateWithContext = { templateRef: TemplateRef; context: T; }; export type ValueOrReactive = | TValue | Observable | Signal; export type PolymorphicPrimitive = ValueOrReactive; export type PolymorphicContent = PolymorphicComponent> | TemplateWithContext | PolymorphicPrimitive; ``` Following this, we define an interface that our wrapper components will adhere to. This interface ensures that any component tasked with wrapping or enhancing content maintains a consistent structure for managing the polymorphic content input: ```ts export interface WithPolymorphicContent { content: InputSignal>; } ``` Finally, let's create the `polymorphic-outlet` component, which will act as the rendering point for any form of content, whether it be a component, a template, a string, etc. This component leverages Angular’s dynamic rendering capabilities to adaptively display content based on its type: ```ts @Component({ standalone: true, selector: 'polymorphic-outlet', imports: [PolymorphicComponentOutletDirective, NgTemplateOutlet], }) export class PolymorphicOutletComponent implements WithPolymorphicContent { public readonly content = input>(); } export const polymorphicOutlet = createPolymorphicComponent(PolymorphicOutletComponent); ``` ```html @switch (true) { @case (isComponent()) { @if (asComponent(); as component) { } } @case (isTemplate()) { @if (asTemplate(); as template) { } } @default { {{ asPrimitive() }} } } ``` Now, let's develop several wrapper components designed to accept content and enhance it with additional visual elements or behaviors. These components will implement the `WithPolymorphicContent` interface, ensuring they possess a content input of the type `InputSignal>`. Each component will utilize the previously defined `polymorphic-outlet` in their templates to dynamically render the provided content: **Badge component:** ```ts @Component({ ... }) export class WithBadgeComponent implements WithPolymorphicContent { public readonly content = input>(); ...// other inputs } ``` ```html ``` **Icon component:** ```ts @Component({ ... }) export class WithIconComponent implements WithPolymorphicContent { public readonly content = input>(); ...// other inputs } ``` ```html {{ icon() }} ``` **Tooltip component:** ```ts @Component({ ... }) export class WithTooltipComponent implements WithPolymorphicContent { public readonly content = input>(); ...// other inputs } ``` ```html ``` Angular 18 introduced a powerful new feature that allows us to pass a fallback value for the `ng-content` component: ```html ``` This feature significantly enhances the flexibility of content projection in Angular. It allows your components to handle both scenarios: projecting content directly via the content input or using Angular's standard content projection mechanism. For example, you can now easily support scenarios where content is provided within the template: ```html Content ``` Or where the content is passed programmatically through the component's content input: ```ts createIconComponent({ inputs: { content: 'Content', }, }); ``` Finally, let's define a function to streamline the composition of views by iterating over a list of wrapper components. Using the `reduce` function, we can apply each wrapper sequentially, thereby enclosing a given content (whether it be a component, template, or string) within all specified wrappers. This method offers extensive customization options, making it easy to combine multiple wrappers around any piece of content: ```ts export const composePolymorphicWrappers = ( ...wrappers: Array>> ) => { return (content: PolymorphicContent): PolymorphicContent => { return wrappers.reduce( (acc, curr) => curr({ inputs: { content: acc, }, }), content ); }; }; ``` ## Putting It All Together Let's consolidate everything discussed so far with a practical example. In this implementation, each wrapper component—`Badge`, `Tooltip`, and `Icon`—is instantiated partially, with specific input properties set upfront: ```ts const withIcon = createWithIconComponentPartial({ inputs: { icon: 'delete', }, outputsHandlers: { iconClicked: () => { inject(MatSnackBar).open('Icon clicked'); }, }, }); const withBadge = createWithBadgeComponentPartial({ inputs: { badge: 'Polymorphic', position: 'above after', }, className: 'd-inline-flex', }); const withTooltip = createWithTooltipComponentPartial({ inputs: { position: 'above', text: 'Polymorphic tooltip', }, providers: [ { provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: { disableTooltipInteractivity: false, }, }, ], }); ``` These initial configurations generate factory functions, which are then coordinated using the `composePolymorphicWrappers` function. This function iterates over an array of wrappers and passes each wrapper itself as the `content` input to the next wrapper in the sequence, culminating in a composite structure. The final step involves passing a specific value to the `wrapContent` function, which then gets wrapped by the combined wrappers: ```ts @Component({ ... imports: [PolymorphicOutletComponent], }) export class MyComponent { public readonly polymorphicView = this.getPolymorphicView(); private getPolymorphicView(): PolymorphicContent { const wrappers: Array>> = [ withBadge, withTooltip, withIcon ]; const wrapContent = composePolymorphicWrappers(...wrappers); return wrapContent('Content'); } } ``` Finally, the `polymorphicView` is rendered through the `polymorphic-outlet` component: ```html ``` ### Polymorphic vs. Imperative Let's compare the traditional imperative approach with the polymorphic approach in the following example: ```html Content ``` The imperative approach, while straightforward, centralizes all logic within the component itself, which can make maintenance difficult as the codebase expands and components grow in complexity. In contrast, the polymorphic approach, by distributing logic across multiple locations, promotes a more flexible architecture that simplifies maintenance through better separation of concerns. Furthermore, the polymorphic method supports dynamic runtime modifications—like reordering components or altering the composition of wrappers—that are unfeasible with the imperative approach. This adaptability is particularly beneficial for complex applications requiring high levels of customization. ![Showcase.gif](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Showcase.gif) ### Modularity of Polymorphic Components with Dynamic Injection Context Binding Using `runInInjectionContext`: Upon revisiting the creation of the icon component, we observe that dependencies are injected directly in the handler function: ```ts const ICON = createWithIconComponentPartial({ inputs: { icon: 'delete', }, outputsHandlers: { iconClicked: () => { inject(MatSnackBar).open('Icon clicked'); }, }, }); ``` This direct injection is made feasible because the output handlers are invoked within the `runInInjectionContext` during component rendering with the `polymorphicComponentOutlet` directive and a value is emitted: ```ts private propagateOutputValue(...): void { ... runInInjectionContext(this.injector, () => { outputHandlers.forEach((handler) => { handler(emittedValue); }) }); ``` The `runInInjectionContext` helper function in Angular empowers execution within a specified injection context, permitting the use of Angular's Dependency Injection (DI) system without being confined to any particular component or injectable class. Such an approach fosters the creation of standalone features that dynamically leverage DI during execution, enhancing both modularity and flexibility. In the realm of polymorphic views, it allows components to dynamically resolve dependencies, thereby ensuring they remain independent, highly adaptable, and reusable across varied contexts. ## Conclusion While the title of this article may have hinted at introducing Functional Components in the style seen in frameworks like React, it's evident that Angular hasn't adopted this paradigm. Nevertheless, the capability to automatically infer input types through signal inputs offers exciting new possibilities. This feature invites us to reimagine component creation in Angular, embracing a hybrid approach that merges class-based components with elements of functional programming, such as currying, partial application, and function composition, enhancing our ability to manage polymorphic views. The benefits of this approach—increased flexibility, improved reusability, simplified composition, and enhanced customization—have been consistently emphasized throughout this article. Additionally, the dynamic injection context binding with `runInInjectionContext` further bolsters this method, enabling the development of more standalone and modular components. Importantly, all these advancements are achieved while preserving strict type safety, a feature still underdeveloped in Angular’s native API for dynamic components. This ongoing evolution opens the door for potential future developments by the Angular team, possibly including native support for functionalities like `asFunctionalComponent`, who knows. ## Showcase 1. [Showcase Component](https://github.com/OleksandrBuchek/ng-concepts/tree/main/libs/demo/polymorphic-content?ref=angularspace.com) ## Source code Here are some references where you can find more information about polymorphic components, polymorphic outlets, and a showcase component in Angular: 1. [Polymorphic Component](https://github.com/OleksandrBuchek/ng-concepts/tree/main/libs/shared/util-polymorphic-content?ref=angularspace.com) 2. [Polymorphic Outlet](https://github.com/OleksandrBuchek/ng-concepts/tree/main/libs/shared/ui-polymorphic-outlet?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Screenshot-2024-09-12-at-15.38.49.png) ### Directive composition API URL: https://www.angularspace.com/directive-composition-api/ Last updated: 2024-09-10T07:14:24.000Z Writing clear and reusable code is an integral part of programming. One of the many tools that Angular offers for developing complex applications is the Directive Composition API. This feature facilitates writing of reusable and maintainable code. In this article, we will explore what the **Directive Composition API** is, how the `hostDirective` option works, and how to successfully use this capability in your Angular projects. ## Introduction Angular Directives are classes that add additional behaviour to HTML elements in your applications. Thanks to the `hostDirective` option, developers can manage directives more efficiently and maintainable. Official documentation about **Directive Composition API** you can find under this [*link*](https://angular.dev/guide/directives/directive-composition-api?ref=angularspace.com). In this article, we will cover: - What is the Directive Composition API? - Understanding the `hostDirective` option - Practical example - Host directive semantics - Best Practices and Tips ## What is Directive composition API? Directive composition API lets you apply directives to a component's host element from within the component's TypeScript class. Thanks to that feature, we can create logic in separated directives and combine them into components. ## Understanding the `hostDirective` option The option is available in angular components and directives. This provides the ability to declare and reuse business logic that can be used in multiple places. You apply directives to a component by adding a `hostDirectives` property to a component's decorator. ```typescript @Directive({ // ... hostDirectives: [UserDirective] }) ``` Directives used in `hostDirectives` must be **Standalone**. I.e., they should have `standalone: true` set in their decorator metadata. For the following code, let's assume that one of `UserDirective`'s tasks is to look for a button in the template to emit `userRemoved` on a `click` event in the user card. ```typescript @Directive({standalone: true}) export class UserDirective implements AfterViewInit { userId = input.required(); userRemoved = output(); private elementRef = inject(ElementRef); private destroyRef = inject(DestroyRef); ngAfterViewInit() { this.userRemoveListener(); } private userRemoveListener() { const removeButton = this.elementRef.nativeElement.querySelector('button.user-remove'); if (!removeButton) return; const listener$ = fromEvent(removeButton, 'click'); listener$ .pipe( takeUntilDestroyed(this.destroyRef) // unsubscribe on destroy ) .subscribe(() => this.userRemoved.emit(this.userId())); } // ... } ``` You can explicitly include `inputs` and `outputs` in your component's API by expanding the entry in ```diff @Component({ selector: 'user-cart', template: ` ... `, standalone: true, - hostDirectives: [UserDirective] + hostDirectives: [ + { + directive: UserDirective, + inputs: ['userId'], + outputs: ['userRemoved'], + }, + ], }) ``` ```html ``` In this way, we can pass data and interactions to our directive through our host. Also, there is a way to use aliases. After declaring our `input` or `output`, we can mark our alias after the colon ```diff @Component({ selector: 'user-cart', template: ` ... `, standalone: true, hostDirectives: [{ directive: UserDirective, + inputs: ['userId: id'], + outputs: ['userRemoved: removed'], }] }) export class UserCartComponent {} ``` ```html ``` ## Practical example [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#practical-example)Let's look at the case below. You may also find a demo app of the following code in this [link](https://stackblitz.com/edit/dc-api?file=src%2Fmain.ts&ref=angularspace.com). We have buttons and tag components. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/example.png) #### Button - Component This is how our component logic looks like. ```typescript @Component({ selector: 'app-btn', template: ``, styleUrls: ['./button.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) export class ButtonComponent { type = input<'basic' | 'soft'>('basic'); variant = input<'primary' | 'secondary'>('primary'); @HostBinding('class') get hostClass(): string { return [this.type(), this.variant()].join(' '); } } ``` #### Tag - Component ```typescript @Component({ selector: 'app-tag', template: ``, styleUrls: ['./tag.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) export class TagComponent { type = input<'basic' | 'soft'>('basic'); variant = input<'primary' | 'secondary'>('primary'); @HostBinding('class') get hostClass(): string { return [this.type(), this.variant()].join(' '); } } ``` Also, a directive used on buttons. #### Disabled - Directive ```typescript @Directive({ selector: '[appBtnDisabled]', standalone: true, }) export class BtnDisabledDirective { disabled = input(false, { transform: booleanAttribute, alias: 'appBtnDisabled', // data pass by selector }); @HostBinding('attr.disabled') get isabledAttr(): '' | null { return this.disabled() ? '' : null; } @HostBinding('class.disabled') get disabledClass(): boolean { return this.disabled(); } @HostListener('click', ['$event']) @HostListener('dbclick', ['$event']) onClick(event: Event): void { if (this.disabled() === false) return; event.preventDefault(); event.stopImmediatePropagation(); } } ``` And we can use these components as follows: ```html Button / Basic primary secondary primary secondary Button / Soft primary secondary primary secondary Tag / Basic primary secondary Tag / Soft primary secondary ``` Everything works fine, but there are some issues with that code: - We have duplicated code between button and tag components. - Nothing prevents us from imposing a `BtnDisabledDirective` on our component tag. And we would like to avoid such a scenario. - For better accessibility, we should apply `disabled` value for `button` tag Let's improve this code a bit! ## How to use hostDirective To begin with, let's start by creating directives to handle the *type* & *variant* appearance of components #### TypeAppearanceDirective ```typescript // Selector is optional, in that case we don't need them @Directive({standalone: true}) export class TypeAppearanceDirective { type = input<'basic' | 'soft'>('basic'); @HostBinding('class') get hostClass(): string { return this.type(); } } ``` [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#typeappearancedirective) [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#typeappearancedirective)[](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#how-to-use-hostdirective) [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#disabled---directive) #### VariantAppearanceDirective ```typescript // Selector is optional, in that case we don't need them @Directive({standalone: true}) export class VariantAppearanceDirective { variant = input<'primary' | 'secondary'>('primary'); @HostBinding('class') get hostClass(): string { return this.variant(); } } ``` **TypeAppearanceDirective** and **VariantAppearanceDirective** could be combined into one directive. So let's create this directive. #### AppearanceDirective ```typescript // Selector is optional, in that case we don't need them @Directive({ standalone: true, hostDirectives: [ { directive: TypeAppearanceDirective, inputs: ['type'], // declare inputs }, { directive: VariantAppearanceDirective, inputs: ['variant'], // declare inputs }, ], }) export class AppearanceDirective {} ``` Declared `inputs` in **AppearanceDirective** will be also available in a component, so we don't need to pass them again in our component, we just need to load our directive. **AppearanceDirective** should be declared into **ButtonComponent** and **TagComponent**. But **BtnDisabledDirective** has to be declared only in **ButtonComponent**. Also, we want to use `disabled` alias for `appBtnDisabled`. There also is a way to `inject` values into our host. In **ButtonComponent** we should also provide `disabled` value into native button, by `disabled` signal property included in **BtnDisabledDirective**, let's mark our injection Let's also mark our `inject` as `self` to inform the Angular that should only look for a value that is bound on the component injector. After small changes, we should have refactored code: #### ButtonComponent ```diff @Component({ selector: 'app-btn', + template: ``, styleUrls: ['./button.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, + hostDirectives: [ + AppearanceDirective, // 'inputs' are declared inside directive + { + directive: BtnDisabledDirective, + inputs: ['appBtnDisabled: disabled'], + }, + ], }) export class ButtonComponent { + disabled = inject(BtnDisabledDirective, { self: true }).disabled; - type = input<'basic' | 'soft'>('basic'); - variant = input<'primary' | 'secondary'>('primary'); - - @HostBinding('class') - get hostClass(): string { - return [this.type(), this.variant()].join(' '); - } } ``` #### TagComponent ```diff @Component({ selector: 'app-tag', template: ``, styleUrls: ['./tag.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, + hostDirectives: [ + AppearanceDirective, // 'inputs' are declared inside directive + ], }) export class TagComponent { - type = input<'basic' | 'soft'>('basic'); - variant = input<'primary' | 'secondary'>('primary'); - - @HostBinding('class') - get hostClass(): string { - return [this.type(), this.variant()].join(' '); - } } ``` and now we are using these components like this ```diff Button / Basic primary secondary -primary +primary -secondary +secondary Button / Soft primary secondary -primary +primary -secondary +secondary Tag / Basic primary secondary Tag / Soft primary secondary ``` The `disabled` input is now an integral part of `ButtonComponent` and we don't need to import `BtnDisabledDirective` in **TagComponent**. ## Host directive semantics ### Directives execution order Host directives just as the other components and directives have the same lifecycle used directly in a template. Remember, host directives always execute their constructor, lifecycle hooks, and bindings before the host. It is important to keep this in mind to be sure and know how to avoid causing our application's performance to suffer. Based on our example with the button component ```typescript @Directive({ standalone: true, hostDirectives: [ { directive: TypeAppearanceDirective, inputs: ['type'], }, { directive: VariantAppearanceDirective, inputs: ['variant'], }, ], }) export class AppearanceDirective {} @Component({ selector: 'app-btn', template: ``, styleUrls: ['./button.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [ AppearanceDirective, { directive: BtnDisabledDirective, inputs: ['appBtnDisabled: disabled'], }, ], }) export class ButtonComponent { // ... } ``` [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#directives-execution-order)the order of execution will look like this: 1. `TypeAppearanceDirective` instantiated 2. `VariantAppearanceDirective` instantiated 3. `AppearanceDirective` instantiated 4. `BtnDisabledDirective` instantiated 5. `ButtonComponent` instantiated 6. `TypeAppearanceDirective` lifecycle hooks 7. `VariantAppearanceDirective` lifecycle hooks 8. `AppearanceDirective` lifecycle hooks 9. `BtnDisabledDirective` lifecycle hooks 10. `ButtonComponent` lifecycle hooks 11. `TypeAppearanceDirective` applies host bindings 12. `VariantAppearanceDirective` applies host bindings 13. `AppearanceDirective` applies host bindings 14. `BtnDisabledDirective` applies host bindings 15. `ButtonComponent` applies host bindings As you can see, that the component and host directives create a new instance each time. In this case, their number is 5, so to maintain good performance of our application, we should not abuse this functionality and implement logic that does not require a lot of computing power. ### Dependency injection Host can inject the instances of host directives that specify `hostDirectives`, and vice versa. Again, based on the example with `ButtonComponent`, we just need to define our Dependency injection by class. ```typescript @Component({ selector: 'app-btn', template: ``, styleUrls: ['./button.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [ // ... { directive: BtnDisabledDirective, inputs: ['appBtnDisabled: disabled'], }, ], }) export class ButtonComponent { disabled = inject(BtnDisabledDirective, {self: true}).disabled } ``` [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#dependency-injection)The providers provided by class with `hostDirectives` take precedence over providers defined by the host directives if a host with `hostDirectives` and those host directives both supply the same injection token. ## Best Practices and Tips - `selector` property is an optional option in directives. If we want to implement directives only in the `hostDirectives` option, we do not need to pass this parameter. - Angular detects whether our directive contains the declared elements in `inputs` and `outputs` so we will be notified if there is a bad implementation. - Use `self` flag while `injecting` directive into host. ## Summary The **Directive Composition API** in Angular offers a way to write reusable and maintainable code. This makes it easy for us to follow the "**Don't repeat yourself**" (*DRY*) principle. However, keep in mind how the code works under the hood to avoid performance problems. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Screenshot-2024-09-10-at-09.13.55.png) ### [JS-POLAND] CONFERENCE TICKETS GIVEAWAY 3x (remote/on-site) URL: https://www.angularspace.com/js-poland-conference-tickets-giveaway-3x-remote-on-site/ Last updated: 2024-09-06T18:30:24.000Z LAST and FINAL Angular Space 3000 Members celebration GIVEAWAY! IMPORTANT: \---> *If you WON NG POLAND Ticket, YOU can't participate.* _This post is for subscribers only._ ### [NG-POLAND] CONFERENCE TICKETS GIVEAWAY 3x (remote/on-site) URL: https://www.angularspace.com/ng-poland-conference-tickets-giveaway-3x-remote-on-site/ Last updated: 2024-09-04T06:19:21.000Z Angular Space 3000 Members celebration continues! _This post is for subscribers only._ ### Create Pagination Using Firebase and NgRx SignalStore URL: https://www.angularspace.com/create-pagination-using-firebase-and-ngrx-signalstore/ Last updated: 2024-09-02T07:31:31.000Z Some time ago, I had the opportunity to start a simple application. Since I was the only developer and it was a charity project, I had the freedom to choose the tools. One of my main goals was to make the most of the newly released signals without losing the power of RxJS. This article is the result of a small part of that project and some of the implementations I made during the process of creating it. But why write an article about pagination, you might be thinking. Because I believe it's a great example of how you can combine signals and observables to get the most out of both. Besides, I couldn't find another tutorial using these technologies on the internet. 😅 ### The Tools 🛠 #### Firebase 🔥 I chose Firebase because it provides everything I need: authentication, storage, and a real-time database. It simplifies my life significantly since I don't have to worry much about the backend. #### NgRx SignalStore🚦 It took me a long time to make a decision, as I generally try to avoid using external libraries whenever possible. However, after reading several articles, comparing other solutions, and conducting some proof-of-concept tests, I chose SignalsStore because it met all my needs. Another valid alternative was [signalsSlice](https://ngxtension.netlify.app/utilities/signals/signal-slice/?ref=angularspace.com), but although it is simpler, it is less powerful than SignalsStore. ### Before the start 🚀 It's not entirely necessary, but if you want to follow the article in more detail, you can clone this [repository](https://github.com/gabrieluy/pagination-firebase-signals-store/?ref=angularspace.com) with the example and check it throughout the article. It's preconfigured with Firebase emulators with preloaded data, and instructions on the README, so you can't complain that I didn't make it easy for you. 😊 If everything goes well, you should see the following: ![App dashboard](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/app-dashboard-2.png) localhost:4200 ![firestore dashboard](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/emulators-dashboard-2.png) localhost:4000/firestore ### The Implementation 🧑‍💻 The idea here is not to create a step-by-step guide, as that would be long and tedious. Instead, I will showcase the key components of the solution and explain the points or decisions that I found to add the most value. If you want to go deeper, you can always check the repository. #### The Post and PostsListConfig interfaces 📝 Lets take a look at the `Post` and `PostsListConfig` interfaces: ```typescript export interface Post { id: string; title: string; description: string; date: number; } export interface PostsListConfig { limit: number; page: number; pageLastElements: Map; } ``` The `Post` interface is a simple object with a few properties, nothing fancy. The `PostsListConfig` interface is more interesting and is used to configure pagination for the posts. Here's a breakdown of its properties: - `limit`: The number of posts to display per page. - `page`: The current page number. - `pageLastElements`: A map storing the last post of each page, which helps manage pagination efficiently. We will go through this later. #### The `PostsService` Class 🔧 Now lets take a look at the `PostsService` class. ```typescript @Injectable({ providedIn: 'root', }) export class PostsService { private readonly PATH = 'posts'; private readonly firestore = inject(Firestore); private readonly collection = collection(this.firestore, this.PATH).withConverter(assignTypes()); getPosts$(config: PostsListConfig): Observable { const { limit: qLimit, page, pageLastElements } = config; const conditions: QueryConstraint[] = [orderBy('date', 'desc'), limit(qLimit)]; let postCollection; if (page === 1) { const { date } = pageLastElements.get(page - 1)!; conditions.push(startAfter(date)); postCollection = query(this.collection, orderBy('date', 'desc'), limit(qLimit)); } else { const { date } = pageLastElements.get(page - 1)!; postCollection = query(this.collection, orderBy('date', 'desc'), limit(qLimit), startAfter(date)); } return collectionData(postCollection, { idField: 'id' }); } } ``` The `PostsService` handles data retrieval from Firestore, it worth to mention that im using AngularFire to simplify the process of interacting with Firestore. We have 2 scenarios here: **Scenario 1: If the page is the first** ```typescript query(this.collection, orderBy('date', 'desc'), limit(qLimit)) ``` Being the first page, no pagination is required, so the query simply orders the posts by date in descending order and limits the results to the specified number (qLimit). This is the simplest scenario. **Scenario 2: If the page is not the first** ```typescript query(this.collection, orderBy('date', 'desc'), limit(qLimit), startAfter(date)) ``` Here, things get more interesting. Firebase doesn't support numerical offsets, so pagination is achieved using [query cursors](https://firebase.google.com/docs/firestore/query-data/query-cursors?ref=angularspace.com). This involves the `startAt` and `startAfter` methods, which require a reference to a document in the database. The difference between the two is: - `startAt`: Returns documents **greater than or equal** to the reference document. - `startAfter`: Returns documents **greater than** the reference document. Similarly, `endAt` and `endBefore` methods can define the end point for the query results. So for be able to navigate forward and backward through the pages, we need to save the last document of each page in the `pageLastElements` map. So now using this reference we can go forward and backward in the collection. An important thing to take into account here is that in in this example the `orderBy` is hardcoded, but in case of dynamic sorting, we should use the order parameter in the `startAfter` methods. #### The `PostsStore` 📦 Let's delve into one of the most interesting parts of this article: The `PostsStore`. The PostsStore holds the state of our application. As mentioned earlier, we will use the `SignalStore` from the `ngrx/store` library to create the store. But first, let's explain the store's model. ```typescript export type StatusType = 'loaded' | 'loading' | 'success' | 'error'; export interface Posts { entities: Post[]; entitiesCount: number; } export interface PostListState { listConfig: PostsListConfig; posts: Posts status: StatusType; } export const postListInitialState: PostListState = { listConfig: { page: 1, limit: 4, pageLastElements: new Map(), }, posts: { entities: [], entitiesCount: 0, }, status: 'loading', }; ``` As you can se from the code above, we have a `PostListState` interface that defines the state of our store. It holds the `listConfig` (which we are already familiar with), the `posts` and the `status`. The `posts` attribute holds the posts and the count of posts, while the status attribute indicates the current status of the store. This status is useful for displaying loaders or error/success messages. The initial state, postListInitialState, simply sets up a starting configuration for the post list. Now that we have our state, we can check the `PostsStore`: ```typescript export const PostsListStore = signalStore( { providedIn: 'root' }, withState(postListInitialState), withComputed(store => ({ paginator: computed(() => ({ show: store.listConfig.page() > 1 || store.posts.entitiesCount() === store.listConfig.limit(), hasPreviousPage: store.listConfig.page() > 1, hasNextPage: store.posts.entitiesCount() === store.listConfig.limit(), })), })), withMethods((store, service = inject(PostsService)) => ({ loadPosts: rxMethod( pipe( tap(() => patchState(store, { status: 'loading' })), switchMap(listConfig => service.getPosts$(listConfig).pipe( tapResponse({ next: (entities: Post[]) => { const newListConfig = listConfig.pageLastElements.set(listConfig.page, entities[entities.length - 1]); patchState(store, { posts: { entitiesCount: entities.length, entities }, listConfig: { ...listConfig, pageLastElements: newListConfig, }, status: 'success', }); }, error: () => { patchState(store, { ...postListInitialState }, { status: 'error' }); }, }) ) ) ) ), })) ); ``` This may look complicated, but it's actually quite simple if we break it down. - The `withState` feature adds state properties to the `SignalStore`, accepting the initial state as an input argument. - The `withComputed` feature is used to add computed properties to the `SignalStore`. - The `withMethods` feature is used to add methods to the `SignalStore`. In our case since the `withState` feature is simple enough, we will not go into detail about it. So let's start with the `withComputed` feature: We use `withComputed` to determine the visibility of the paginator based on the current page and the number of entities in the store. This ensures that the paginator buttons are displayed only when needed, which we will see in action later. For the `withMethods` feature, we use [rxMethod](https://ngrx.io/api/store/rx-method?ref=angularspace.com) to create a method called `loadPosts`. This method receives a `PostsListConfig` object and returns an `Observable` that loads the posts based on the configuration. If we look closely at the `tapResponse`, we can see how we update the pageLastElements map and then add it to the state using `patchState`. Additionally, we manage the status by setting it to `loading` initially, and then to `success` or `error` based on the outcome of the data fetch. #### The `PostsComponent` 💻 Now we can start with the most exciting part of the project: using our store to display the posts. One of the best things about `SignalStore` is that it is incredibly easy to use—we only need to inject it and utilize its features. We can consume the state signals like `posts` or call methods like `loadPosts`. Additionally, we can use the computed function to create a computed property that updates when the store changes. In our case, we have `$isLoading`. ```typescript export class PostsListComponent { readonly listStore = inject(PostsListStore); $posts = this.listStore.posts.entities; $isLoading = computed(() => this.listStore.status() === 'loading'); $listConfig = this.listStore.listConfig; constructor() { this.listStore.loadPosts(this.$listConfig()); } goToNextPage() { this.listStore.loadPosts({ ...this.$listConfig(), page: this.$listConfig.page() + 1 }); window.scroll({ top: 0, left: 0, behavior: 'smooth' }); } goToPrevPage() { this.listStore.loadPosts({ ...this.$listConfig(), page: this.$listConfig.page() - 1 }); window.scroll({ top: 0, left: 0, behavior: 'smooth' }); } } ``` ```html
@if (!$isLoading()) { @for (post of $posts().values(); track post.id) { } } @else { }
``` #### The `PaginatorComponent` 💻 To wrap up this article, as promised, let's examine the `PaginatorComponent`. This component uses signal inputs and outputs to manage the visibility of the paginator and the buttons for navigating to the previous and next pages. As you can see, the component is quite straightforward and easy to understand. ```html @if ($paginator().show) {
} ``` ```typescript export class PaginatorComponent { $paginator = input.required(); onNextClicked = output(); onPrevClicked = output(); } ``` #### The Demo 🎨 If you haven't downloaded the repository, you can view the demo below. Notice that because we are using a rxMethod, the loaded posts remain connected with the Firebase database, allowing you to see changes in real time. In case you haven't downloaded the repository, you can view the demo below: Notice that because we are using a `rxMethod` the loaded posts remain connected with the firebase database, so you can see the changes in real time. 0:00 /0:15 1× Final demo Thank you for following along! I hope this guide helps you create pagination using Firebase in the future and, more importantly, improves your skills with Signals. And remember, you can check the [repository](https://github.com/gabrieluy/pagination-firebase-signals-store/?ref=angularspace.com) to view the code and run the demo yourself. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Screenshot-2024-08-05-at-10.51.46.jpg) ### 1x ON-SITE NG-DE CONFERENCE TICKET GIVEAWAY URL: https://www.angularspace.com/1x-on-site-ng-de-conference-ticket-giveaway/ Last updated: 2024-09-25T13:44:08.000Z Can't miss this one !! _This post is for subscribers only._ ### JavaScript and PHP are (not) functional programming languages, and they are (not) safe to write applications using a Functional programming paradigm. URL: https://www.angularspace.com/javascript-and-php-are-not-functional-programming-languages/ Last updated: 2024-08-28T12:25:34.000Z In this article, we will analyze JavaScript and PHP languages and their support for functional programming paradigm. However, to do so, we will first analyze if those languages are Object-Oriented, and if they are, how can we be certain of that? Through analysis of their support for the Object-Oriented paradigm, we will establish a systematic approach and critical thinking in the evaluation of the properties of one language to be able to classify it in a certain paradigm. We are starting with the Object-Oriented paradigm because there is no (too much) controversy when it comes to PHP, JavaScript and Object-Oriented paradigm. ## Support for Object-Oriented paradigm JavaScript and PHP are similar in that they do not have a compilation phase in design time, rather they are interpreted languages. Nowadays, they do have a compilation phase in runtime for the purpose of execution optimization, but that is not important for this article. In order for some language to be considered Object-Oriented, it must support certain features. Those features are: **inheritance**, **encapsulation**, **polymorphism** and **abstraction**. If JavaScript and PHP support those features, we can safely say they are Object-Oriented languages. So, let's see if they do. ### Inheritance Inheritance is a mechanism in which a new class is derived from an existing class [\[1\]](#1-httpsenwikipediaorgwikiinheritance%5Fobject-oriented%5Fprogramming). In both JavaScript and PHP, we achieve inheritance by using the `extends` keyword, so here are some code snippets that demonstrate inheritance in both of the languages. ```javascript // JavaScript class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Dog extends Animal { speak() { console.log(`${this.name} barks.`); } } ``` ```javascript // PHP class Animal { public $name; public function __construct($name) { $this->name = $name; } public function speak() { echo "{$this->name} makes a noise."; } } class Dog extends Animal { public function speak() { echo "{$this->name} barks."; } } ``` One could argue that JavaScript does not support class-based inheritance, but rather it supports prototypal inheritance[\[2\]](#2-httpsenwikipediaorgwikiprototype-based%5Fprogramming). However, class-based inheritance is not a requirement for inheritance. Both class-based and prototypal inheritances are valid ways to achieve it. It is interesting that [**Go**](https://go.dev/?ref=angularspace.com) language does not have inheritance, but it is still considered an Object-Oriented language (well, kind of) [\[3\]](#3-httpsgodevdocfaqis%5Fgo%5Fan%5Fobject-oriented%5Flanguage). Considering that "composition over inheritance" is a common practice in modern software development, it is no wonder why some of the experts in the field are challenging the importance and requirement of inheritance in OOP [\[4\]](#4-httpswwwyoutubecomwatchvxcpslrpomjm). However, without any doubt, both JavaScript and PHP support inheritance. ### Encapsulation Encapsulation is a property of OOP that essentially prevents external code from being concerned with the internal workings of an object[\[5\]](#5-httpsenwikipediaorgwikiencapsulation%5Fcomputer%5Fprogramming). Simply put, it is a way to hide properties/methods from the outside world. In JavaScript, we have `private` and `public` keywords to achieve encapsulation, while in PHP we have `private`, `protected` and `public`. We call them **access modifiers** ("member visibility" is used as a term as well, and there are a few more terms used out there in the wild). ```javascript // JavaScript class Animal { #privateProperty = 'private'; publicProperty = 'public'; #privateMethod() { /../ } publicMethod() { /../ } } ``` ```javascript // PHP class Animal { private $privateProperty = 'private'; protected $protectedProperty = 'protected'; public $publicProperty = 'public'; private function privateMethod() { /../ } protected function protectedMethod() { /../ } public function publicMethod() { /../ } } ``` One could argue that JavaScript does not have `protected` keyword, but it is not important for the "encapsulation" box to be ticked because encapsulation does not require access modifiers, those are just implementation details of the languages. Encapsulation only requires the possibility to hide "internal workings of an object", which both languages provide. Therefore, it is obvious that both JavaScript and PHP support encapsulation. **NOTE**: Prior to the `class` syntax in JavaScript, we used the `constructor` function to achieve encapsulation[\[6\]](#6-httpswwwcrockfordcomjavascriptprivatehtml), and even that was considered as valid encapsulation. Moreover, no one has any doubt that Python is an Object-Oriented language, even though it does not have access modifiers at all and encapsulation is achieved in userland with notation (by using an underscore prefix). ### Polymorphism Polymorphism requires the possibility to represent multiple types with a single symbol[\[7\]](#7-httpsenwikipediaorgwikipolymorphism%5Fcomputer%5Fscience). This definition is a bit vague, as polymorphism is quite a complex topic. The most common forms of polymorphism are ad-hoc polymorphism[\[8\]](#8-httpsenwikipediaorgwikiad%5Fhoc%5Fpolymorphism), parametric polymorphism[\[9\]](#9-httpsenwikipediaorgwikiparametric%5Fpolymorphism), and subtype polymorphism[\[10\]](#10-httpsenwikipediaorgwikisubtyping). Going into detail about each form of polymorphism is out of the scope of this article, but if you are interested in a topic, I encourage you to read more about it starting from provided references[\[7\]](#7-httpsenwikipediaorgwikipolymorphism%5Fcomputer%5Fscience)[\[8\]](#8-httpsenwikipediaorgwikiad%5Fhoc%5Fpolymorphism)[\[9\]](#9-httpsenwikipediaorgwikiparametric%5Fpolymorphism)[\[10\]](#10-httpsenwikipediaorgwikisubtyping). Needless to say, both JavaScript and PHP support polymorphism, as they are dynamically typed languages. In general, all dynamically typed languages are polymorphic by their nature. Previously stated can be easily fact-checked by going through provided references, which I encourage you to do. ### Abstraction During my university studies, I was taught that Object-Oriented Programming requires inheritance, encapsulation, and polymorphism. We called that "Holy threesome of OOP" (sounds dirty, I know). No abstraction was mentioned. Simplified, abstraction refers to the ability to hide complex implementation details and show only the necessary features of an object[\[11\]](#11-httpsenwikipediaorgwikiobject-oriented%5Fprogramming). Intuitively, one can easily conclude that abstraction in Object-Oriented programming is a direct consequence of encapsulation, polymorphism, and inheritance (which can be a reason why during my studies we had "Holy threesome", not "Holy foursome"). Commonly, abstraction in Object-Oriented programming language is achieved by using interfaces and abstract classes. PHP supports interfaces and abstract classes, so there is no doubt that PHP supports abstraction. ```javascript // PHP interface Animal { public function speak(); } class Dog implements Animal { public function speak() { echo 'Dog barks.'; } } ``` ```javascript // PHP abstract class Animal { public abstract function speak(); } class Dog extends Animal { public function speak() { echo 'Dog barks.'; } } ``` Abstraction in typical class-based Object-Oriented languages depends on subtype polymorphism[\[10\]](#10-httpsenwikipediaorgwikisubtyping), of course. Because of that, we can write code that depends on general types (abstractions, contracts), not on concrete implementations. Hence, the core foundation of "Dependency Inversion Principle"[\[12\]](#12-httpsenwikipediaorgwikidependency%5Finversion%5Fprinciple) from SOLID principles in Object-Oriented programming[\[13\]](#13-httpsenwikipediaorgwikisolid) is an abstraction - "Depend upon abstractions, not concrete implementations". ```javascript // PHP class Shelter { private $animals = []; public function addAnimal(Animal $animal) { $this->animals[] = $animal; } } $shelter = new Shelter(); $dog = new Dog(); $shelter->addAnimal($dog); ``` JavaScript, however, does not support interfaces and abstract classes and one could easily argue that JavaScript does not support abstraction. But that is not entirely true, it can be achieved in userland. There is always a naive approach called "duck typing" (*If it walks like a duck, and it quacks like a duck, then it must* *be a duck*)[\[14\]](#14-httpsenwikipediaorgwikiduck%5Ftyping). In general, it means that an object instance can be checked for certain properties/methods and if it has them, it can be considered as an "instance" of a certain "interface" or " abstract class". However, this is just a bad idea since it is very error-prone. ```javascript // JavaScript class Parrot { fly() { console.log('Fly my bird, fly!.'); } } class Airplane { fly() { console.log('Engine started!'); } } for (const something of [new Parrot(), new Airplane()]) { if (typeof something.fly === 'function') { console.log('It is a bird!'); // Not true for airplane } } ``` JavaScript supports the `instanceof` operator, which can be used to check if an object is an instance of a certain class. Well, not exactly an instance of the class, it will check if the given class constructor/function is in the prototype chain of the given object[\[15\]](#15-httpsdevelopermozillaorgen-usdocswebjavascriptreferenceoperatorsinstanceof). So all we need to do is to mimic interfaces and abstract classes in JavaScript. ```javascript // JavaScript /** * @interface */ class Animal { constructor() { throw new Error('Interface "Animal" cannot be instantiated.'); } speak() { throw new Error('Method of interface "Animal" must be implemented.'); } } /** * @abstract */ class Car { constructor(hasGasoline) { this.#hasGasoline = hasGasoline; } start() { if (!this.#hasGasoline) { throw new Error('Car has no gasoline.'); } console.log('Engine started.'); } break() { throw new Error('Method of interface "Animal" must be implemented.'); } } class Parrot extends Animal { constructor() { super.constructor(); } speak() { console.log('Parrot speaks.'); } } class Lambo extends Car { break() { console.log('The Father has stopped the car.'); } } for (const something of [new Parrot(), new Lambo()]) { if (something instanceof Animal) { console.log('It is animal!'); } } ``` So, if Object-Oriented programming requires a "Holy foursome" and not only a "Holy threesome", JavaScript still can be considered as a language that supports abstraction through subtype polymorphism, just not on the language level. ### Verdict We have established criteria for evaluating if one language can be considered as a language that supports Object-Oriented paradigm. By evaluating each language against each requirement, we can easily conclude that both JavaScript and PHP are Object-Oriented languages, there is no doubt about that. All required boxes are ticked. The last one seems debatable, JavaScript does not support interfaces and abstract classes on a language level. If we formulate the statement "JavaScript prevents us from having interfaces and abstract classes", it would be false. We can mimic them, we know that it is possible, and this article contains code that proves it. So if it is possible, it is supported. However, the most important here is that we introduced a systematic approach and mental model of how to evaluate if a certain language supports a certain paradigm. This is important because there is a lot of fallacy when it comes to functional programming paradigm and JavaScript and PHP. **NOTE**: According to the Alan Key[\[16\]](#16-httpsenwikipediaorgwikialan%5Fkay), the father of Object-Oriented paradigm and Smalltalk programming language, we missed the key point of Object-Oriented paradigm, which is state encapsulation and message passing[\[17\]](#17-httpswikic2comalankayonmessaging). ## Support for Functional programming paradigm While in Object-Oriented paradigm programs are organized around objects, their composition, and their interactions ( messages), in a Functional programming paradigm programs are organized around functions and their composition. Origins of Functional programming paradigm are in Lambda calculus, established by Alonzo Church in the 1930s[\[18\]](#18-httpsenwikipediaorgwikilambda%5Fcalculus), but the year 1950 can be considered the year of birth of the Functional programming paradigm with LISP language. Similarly to the Object-Oriented programming paradigm, a Functional programming paradigm has its own set of features that a language must support to be considered a functional programming language. Those features are **first-class** **functions and higher-order functions**, **referential transparency and immutability**, **pure functions**, **functional** **data structures**, **lazy evaluation**, and **recursion**. There is also requirement for **type system** which can be found in the literature, it will be evaluated in this article, although type system is a feature of the language that prevents us from writing invalid programs which could error out in runtime. ### First-class functions and higher-order functions When language supports first-class functions, it means that functions can appear anywhere in the code, including being passed as arguments to other functions, returned as values from other functions, and assigned to variables[\[19\]](#19-httpsenwikipediaorgwikifirst-class%5Ffunction). Higher-order functions are functions that can take other functions as arguments or return them as results[\[20\]](#20-httpsenwikipediaorgwikihigher-order%5Ffunction). Both JavaScript and PHP support first-class functions and higher-order functions. ```javascript // JavaScript const add = (a, b) => a + b; const operator = (a, b, operation) => operation(a, b); console.log(operator(1, 2, add)); ``` ```javascript // PHP $add = static fn ($a, $b) => $a + $b; $operator = static fn ($a, $b, $operation) => $operation($a, $b); echo $operator(1, 2, $add); ``` The examples above do not demonstrate a function returning function, but it is possible in both languages. So we can conclude without any doubt that both JavaScript and PHP support first-class functions and higher-order functions. This is a crucial property of a Functional programming paradigm as it is necessary for the composition of functions. ### Referential transparency and immutability Simply put, referential transparency and immutability mean that once allocated memory is assigned to some value, it can never change, it can be freed only. This is important because it prevents side effects. There are nice consequences of referential transparency which can be exploited: - **No side effects** \- since memory is immutable, there are no side effects. This implies that there is "no (mutable) state". There is always a state of the memory, of course, but the state can not change, hence the slogan "no state, no side effects" of a Functional programming paradigm. This is important because it makes programs easier to reason about, easier to test, and easier to debug. - **Thread safety** \- since there are no side effects, there is no need for locks, semaphores, mutexes, and other synchronization primitives. Every thread can simultaneously access a given memory location. However, some notes must be made here, and those are related to some misconceptions about immutability and what is allowed. So here are some expressions which are not allowed in Functional programming paradigm, but you will most likely see in "functional" JavaScript and PHP code: ```javascript // JavaScript function foo(a) { let b = a++; // Not a functional programming } ``` Expressions such as `a++`, `a--` are not allowed in functional programming paradigm, they are mutating the memory. ```javascript // PHP for ($i = 1; i++; $i < 10) { // Not a functional programming } ``` Loops are invalid in a Functional programming paradigm, they depend on mutable state. JavaScript has `const` keyword which is used to declare `readonly` primitive variables, such as numbers, strings, and booleans. However, `const` keyword does not make the object/array immutable, it only makes reference to the object/array immutable. PHP does not have such language construct. However, that is not important at all. If referential transparency and immutability are required for the Functional programming paradigm, we can achieve it in userland, either through discipline, or by using linters and static code analysis tools, or both. So if we can write our code without mutating allocated memory locations, we can say that language supports referential transparency and immutability. ### Pure functions Pure functions are functions that have no side effects and return value depends only on its arguments. Since referential transparency and immutability are required for a Functional programming paradigm, pure functions are a natural consequence of those two properties. And they have some really nice properties: - **Memoization** \- calling a function with the same arguments will always return the same result. This means that the function results can be safely cached, practically forever[\[21\]](#21-httpsenwikipediaorgwikimemoization). - **Thread safety** \- a natural consequence of referential transparency and immutability. - **Easy to test** \- since there are no side effects, testing is easy. - **Reordering of execution** \- If there is no data dependency between two functions, their order of execution can be changed, or they can be executed in parallel. Neither PHP nor JavaScript has language constructs that enforce pure functions, but that does not prevent us from writing them. Therefore, both languages support pure functions. Do note that using memory addresses from outer scope does not make function impure. As long as there are no states and no effects function is pure. ```javascript // JavaScript function foo(bar) { return function (baz) { return bar + baz; } } ``` ```javascript // PHP function foo($bar) { return static fn ($baz) => $bar + $baz; } ``` Both functions are pure, even though they are returning functions that are using memory addresses from outer scope. The reason for that is that there are no side effects, and the return value depends only on arguments. Functions in examples are thread-safe, can be memoized, can be reordered in execution, and are easy to test. ### Type system Type system in my own opinion should not be required for functional programming paradigm, but it is a useful feature required for writing safe programs. PHP has (an optional) type system, to some extent (generics are not supported, for example), while JavaScript does not have a type system. However, if type checking is required, it can be achieved in userland (for example, via specialized tools for static code analysis, such as Psalm[\[22\]](#22-httpspsalmdev) and PHPStan[\[23\]](#23-httpsphpstanorg) in the PHP ecosystem). ### Functional data structures Functional data structures are immutable data structures. That would mean that both PHP and JavaScript would have to have, for example, immutable arrays and objects. Neither supports that on the language level. This requirement comes from the fact that functional programming languages that do support functional data structures are optimized for them because immutability is expensive. Again, no one prevents you to create and use immutable data structures in your JavaScript and PHP code. So, it is not supported on the language level, but it is supported in userland. ### Lazy evaluation Lazy evaluation comes from the fact that functional programming languages are slower than imperative programming. Lazy evaluation is a technique where expression is not evaluated until it is needed, which should improve performance. Take a look at this example: ```javascript // JavaScript let arr = [1, 2 / 0, 3]; console.log(arr.length); ``` ```javascript // PHP $arr = [1, 2/0, 3]; echo count($arr); ``` Both languages will throw an error, but in a functional programming language that supports lazy evaluation expression `2/0` would not be evaluated until it is needed (for example `arr[1]`). Still, even though PHP and JavaScript do not support lazy evaluation on the language level, and can not be achieved in userland, it does not prevent us from writing programs that use functional programming paradigm. The lack of this feature should not be a deal-breaker for functional programming as it only impacts execution performances, and maybe when a runtime error would occur due to an incorrect program. ### Recursion Recursion is a technique where a function calls itself. Since a Functional programming paradigm is based on functions and does not allow loops, recursion is required. Both PHP and JavaScript support recursion. However, there is a problem in both of the languages, and that is stack overflow. The best way to explain this is through example. Let's say that we have some number `n` and we want to calculate the sum of all successive numbers from `1` to `n`. If we have 5, the sum would be `1 + 2 + 3 + 4 + 5 = 15`. The usual approach would be to write a simple loop: ```javascript // JavaScript function sum(n) { let result = 0; for (let i = 1; i <= n; i++) { result += i; } return result; } ``` ```javascript // PHP function sum($n) { $result = 0; for ($i = 1; $i <= $n; $i++) { $result += $i; } return $result; } ``` Given code examples violate a Functional programming paradigm, we are mutating memory. So, let's write a recursive function: ```javascript // JavaScript function sum(n, carry = 0) { if (n === 1) { return 1 + carry; } return sum(n - 1, n + carry); } ``` ```php // PHP function sum($n, $carry = 0) { if ($n === 1) { return 1 + $carry; } return sum($n - 1, $n + $carry); } ``` Now, these are pure functions which do not mutate memory. However, if we call `sum(100000)` in both languages, we will get a stack overflow error. This is because both languages do not support tail call optimization[\[24\]](#24-httpsenwikipediaorgwikitail%5Fcall). Tail call optimization is a technique where the compiler recognizes that the function call is the last operation in the function, removes the current stack frame, and replaces it with a new one. So if we have `sum(100000)` in tail call optimized language, we would not get a stack overflow error because there would be only one stack frame. ### Verdict We have established criteria for evaluating if one language can be considered as a language that supports the Functional programming paradigm. Neither PHP nor JavaScript supports all required features on the language level, but most of those are either debatable (type system, lazy evaluation) or can be achieved in userland. However, due to a lack of tail call optimization, we just can not safely write our application in JavaScript and PHP using a Functional programming paradigm because the successful execution of the program depends on the input size. Only one recursive call with a large input size can crash the program. This **must** be impossible in functional programming languages. Since this is not possible in JavaScript and PHP, they do not fully support a Functional programming paradigm. The range of problems that can be solved safely using these languages with a Functional programming paradigm is limited. ## Final thoughts The problem with the lack of tail call optimization in JavaScript and PHP can be solved by replacing recursion with loops. If we do so, we will have so-called "impure functions"[\[25\]](#25-httpsenwikipediaorgwikipure%5Ffunction-) in our functional programming code. Impure functions are functions that have side effects, that is, imperative style programming is allowed. If impure functions are allowed in a Functional programming paradigm, and if we are using them in our functional program, that means that if we add pure functions to our imperative program, can we claim that we have an "impure imperative" program (which is ridiculous statement)? The whole point of a Functional programming paradigm and all the goodies which come from functional programming comes from the pure functions and "no state, no side effects". Contaminating every (or most of the functions) with state and side effects makes a Functional programming paradigm useless and claiming that we are writing functional programming code is a fallacy. Unfortunately, that happens all the time. People are claiming that they are writing applications using a Functional programming paradigm, especially when it comes to front-end development. However, if you take a look at their code, state and side effects are everywhere. You will even find `this` keyword in functions with the claim that functions follow a Functional programming paradigm. Even when using a Functional programming paradigm, you will eventually have to deal with the state. How you will deal with it will determine if you are using a Functional programming paradigm, or not. Think about that like spaghetti (pasta). You love them, they are tasty, but it has dirt all over them. That is the state and side effects in a program written by using "functional programming". If that "dirt" is in the plate, but there is a little of it, and there is a strong boundary between spaghetti and dirt (dirt is not sprinkled all over the spaghetti), most of the time you can enjoy spaghetti without dealing with dirt. Knowledge is crucial as well. By knowing the limitation in JavaScript and PHP in terms of tail call optimization, you know that this function is unsafe: ```javascript // JavaScript function sum(n, carry = 0) { if (n === 1) { return 1 + carry; } return sum(n - 1, n + carry); } ``` However, you can replace it with an impure equivalent: ```javascript // JavaScript function sum(n) { let result = 0; for (let i = 1; i <= n; i++) { result += i; } return result; } ``` From the outside world, this function can be considered pure. It is thread-safe, can be run in parallel, can be reordered in execution, can be memoized, and is easy to test. But how you decide to implement it, will determine if you are using a Functional programming paradigm. If a decision is based on the knowledge that JavaScript/PHP does not support tail call optimization and input size can be large so stack overflow can happen, then we are talking about the informed decision, and your function, even though it is not pure, can be considered as such. If you have written it without knowing that, then you are just writing imperative code. ## References ###### \[1\] [https://en.wikipedia.org/wiki/Inheritance\_(object-oriented\_programming)](https://en.wikipedia.org/wiki/Inheritance%5F%28object-oriented%5Fprogramming%29?ref=angularspace.com) ###### \[2\] [https://en.wikipedia.org/wiki/Prototype-based\_programming](https://en.wikipedia.org/wiki/Prototype-based%5Fprogramming?ref=angularspace.com) ###### \[3\] [https://go.dev/doc/faq#Is\_Go\_an\_object-oriented\_language](https://go.dev/doc/faq?ref=angularspace.com#Is%5FGo%5Fan%5Fobject-oriented%5Flanguage) ###### \[4\] [https://www.youtube.com/watch?v=xcpSLRpOMJM](https://www.youtube.com/watch?v=xcpSLRpOMJM&ref=angularspace.com) ###### \[5\] [https://en.wikipedia.org/wiki/Encapsulation\_(computer\_programming)](https://en.wikipedia.org/wiki/Encapsulation%5F%28computer%5Fprogramming%29?ref=angularspace.com) ###### \[6\] [https://www.crockford.com/javascript/private.html](https://www.crockford.com/javascript/private.html?ref=angularspace.com) ###### \[7\] [https://en.wikipedia.org/wiki/Polymorphism\_(computer\_science)](https://en.wikipedia.org/wiki/Polymorphism%5F%28computer%5Fscience%29?ref=angularspace.com) ###### \[8\] [https://en.wikipedia.org/wiki/Ad\_hoc\_polymorphism](https://en.wikipedia.org/wiki/Ad%5Fhoc%5Fpolymorphism?ref=angularspace.com) ###### \[9\] [https://en.wikipedia.org/wiki/Parametric\_polymorphism](https://en.wikipedia.org/wiki/Parametric%5Fpolymorphism?ref=angularspace.com) ###### \[10\] [https://en.wikipedia.org/wiki/Subtyping](https://en.wikipedia.org/wiki/Subtyping?ref=angularspace.com) ###### \[11\] [https://en.wikipedia.org/wiki/Object-oriented\_programming](https://en.wikipedia.org/wiki/Object-oriented%5Fprogramming?ref=angularspace.com) ###### \[12\] [https://en.wikipedia.org/wiki/Dependency\_inversion\_principle](https://en.wikipedia.org/wiki/Dependency%5Finversion%5Fprinciple?ref=angularspace.com) ###### \[13\] [https://en.wikipedia.org/wiki/SOLID](https://en.wikipedia.org/wiki/SOLID?ref=angularspace.com) ###### \[14\] [https://en.wikipedia.org/wiki/Duck\_typing](https://en.wikipedia.org/wiki/Duck%5Ftyping?ref=angularspace.com) ###### \[15\] [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof?ref=angularspace.com) ###### \[16\] [https://en.wikipedia.org/wiki/Alan\_Kay](https://en.wikipedia.org/wiki/Alan%5FKay?ref=angularspace.com) ###### \[17\] [https://wiki.c2.com/?AlanKayOnMessaging](https://wiki.c2.com/?AlanKayOnMessaging&ref=angularspace.com) ###### \[18\] [https://en.wikipedia.org/wiki/Lambda\_calculus](https://en.wikipedia.org/wiki/Lambda%5Fcalculus?ref=angularspace.com) ###### \[19\] [https://en.wikipedia.org/wiki/First-class\_function](https://en.wikipedia.org/wiki/First-class%5Ffunction?ref=angularspace.com) ###### \[20\] [https://en.wikipedia.org/wiki/Higher-order\_function](https://en.wikipedia.org/wiki/Higher-order%5Ffunction?ref=angularspace.com) ###### \[21\] [https://en.wikipedia.org/wiki/Memoization](https://en.wikipedia.org/wiki/Memoization?ref=angularspace.com) ###### \[22\] [https://psalm.dev](https://psalm.dev/?ref=angularspace.com) ###### \[23\] [https://phpstan.org](https://phpstan.org/?ref=angularspace.com) ###### \[24\] [https://en.wikipedia.org/wiki/Tail\_call](https://en.wikipedia.org/wiki/Tail%5Fcall?ref=angularspace.com) ###### \[25\] [https://en.wikipedia.org/wiki/Pure\_function](https://en.wikipedia.org/wiki/Pure%5Ffunction?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/Screenshot-2024-08-28-at-09.39.59-1.png) ### NEW Open Source Mission Utility URL: https://www.angularspace.com/new-open-source-mission-utility/ Last updated: 2024-08-24T15:59:01.000Z Exciting news! Angular Space launched a new OSS Missions channel on Discord! \- Contribute to open-source projects. \- Complete missions. \- Earn points. For Points Redeem rewards like \- books, \- mentoring hours \- social media promotion Here is what we currently have in stock of Discord shop: 0:00 /0:16 1× To participate Join Discord - first mission is already waiting 😄 Discord link for registered users below. _This post is for subscribers only._ ### 🔎 Deep Dive into Nx Affected URL: https://www.angularspace.com/deep-dive-into-nx-affected/ Last updated: 2024-08-21T03:09:59.000Z ## 😵‍ **Why is this untouched project affected?** This is a question I hear every day! A question that has led me many times into a debugging session of the [**Nx Affected**](https://nx.dev/ci/features/affected?ref=angularspace.com) process to find an answer. In this article, I aim to provide you with all the necessary insights to understand how the affected process of Nx works, helping you answer that very question. ## 🤓 Affected Reminder [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#-affected-reminder) When working on a **large code base** in a Monorepo, you’ll have a repository that contains multiple Applications and Libraries. As your Monorepo grows, **rebuilding all apps/libs** can become **slow** on the CI. The ability to re-execute **only the impacted** apps/libs will drastically **reduce your Software Development Cycle time**. To be able to compute the affected projects, Nx will generate an Affected Graph with all affected projects and their dependencies. More info in [this clip made by Zack](https://www.youtube.com/shorts/m2vagUiiArM?ref=angularspace.com) ### Affected Projects [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#affected-projects) Any **change** applied to app/lib will cascade affected flags on all other **apps/libs** that **depend** on it: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/01-affected-projects.png) To understand the dependencies between apps/libs, Nx generates a [Project Graph](https://nx.dev/concepts/mental-model?ref=angularspace.com#the-project-graph) with all **nodes** (apps/libs), the external nodes (npm), and all **dependencies** between them. ### Affected Tasks [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#affected-tasks) Considering the impact of a **modification** on the entire app/lib is not enough. For instance, if you **change a test** within an app, it does not mean you should re-build that app entirely. Only the **test** should be **re-run:** ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/02-affected-graphs.png) To understand the task dependencies between apps/libs, Nx generates a [Task Graph](https://nx.dev/concepts/mental-model?ref=angularspace.com#the-task-graph) with nodes representing an **association** of the app/lib **by tasks**. ## 🤩 Affected Commands [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#-affected-commands) Nx provides multiple ways to identify which projects/tasks are affected. ### Affected Run [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#affected-run) The **main command** used typically on the CI is the [Nx affected command](https://nx.dev/nx-api/nx/documents/affected?ref=angularspace.com): ```BASH nx affected -t lint test build ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/03-affected-run.png) With this command, you’ll be able to run only the list of tasks that are affected. ### Show Command [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#show-command) Another useful command to get a direct overview is the [Nx show command](https://nx.dev/nx-api/nx/documents/show?ref=angularspace.com): ```BASH nx show projects --affected ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/04-show-command.png) This command allows you to see the affected projects/tasks directly in the console and also export the result to a `JSON` file, for example. ### Nx Graph [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#nx-graph) If you need a UI visualization and a way to track the path of the affected projects/tasks, you can open the [Nx graph](https://nx.dev/nx-api/nx/documents/dep-graph?ref=angularspace.com) by using the command: ```BASH nx graph --affected ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/05-affected-graph-command.png) It will open a web page where you can see a graph similar to this: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/06-affected-graph.png) ## 😶‍🌫️ Affected Rules [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#%EF%B8%8F-affected-rules) The Nx affected process goes through several steps and considers various files and configurations to decide which project can be affected: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/07-affected-rules-overview.png) ### Step 1 - Find Touched Files [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#step-1---find-touched-files) Before computing the list of affected projects, Nx loads the list of modified/touched files: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/08-find-touch-files.png) Nx computes that list by taking the modified files since the targeted **Affected Base**. By **default**, the base will be your **main branch**, but you can modify it using the `-base` and `-head` options. All modified files **not yetcommitted** or **tracked** will also be added. You can change the behavior using the `-uncommitted` or `-untracked` options. If you **don't want Nx to compute** the list of files, you can provide your list using the `-files` option. Files matching a pattern in `.gitignore` or `.nxignore` will be **ignored**. ### Step 2.1 - Find Affected Nodes from Paths [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#step-21---find-affected-nodes-from-paths) When all touched files are defined, Nx checks how they can affect the projects: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/09-find-affected-nodes.png) The most common rule is to check if the **file** path **matches** the **project root** path. ### Step 2.2 - Find Affected Nodes from Tasks [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#step-22---find-affected-nodes-from-tasks) Nx will also implicitly affect some projects if one of their tasks is impacted by a touched file: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/11-find-affected-tasks.png) For example, if you modify a global Jest configuration file like `jest.preset.ts`, because it is specified in the inputs related to that target, you will also affect all projects using that executor: ```JSON "targetDefaults": { "@nx/jest:jest": { "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.ts"], }, }, ``` Each time a project contains a target with an input affected by a touched file, it will be impacted. > For more detailed information about how Nx handles target caching through [Inputs](https://nx.dev/reference/inputs?ref=angularspace.com#inputs-and-named-inputs), [Named Inputs](https://nx.dev/reference/inputs?ref=angularspace.com#inputs-and-named-inputs) and [Outputs](https://nx.dev/recipes/running-tasks/configure-outputs?ref=angularspace.com#configure-outputs-for-task-caching), refer to the [Nx documentation](https://nx.dev/recipes/running-tasks/configure-inputs?ref=angularspace.com) ### Step 2.3 - Find Affected Nodes from Plugins [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#step-23---find-affected-nodes-from-plugins) Since the [Nx Project Crystal](https://nx.dev/concepts/inferred-tasks?ref=angularspace.com#inferred-tasks-project-crystal) and the generalization of Nx Plugins with inferred configurations, Nx also checks if a plugin pattern is affected by the list of touched files. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/12-affected-plugins.png) For example, if you delete or move a file, Nx assumes the project is deleted and marks all projects as affected. ### Step 2.4 - Find Affected Nodes from Npm Dependencies [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#step-24---find-affected-nodes-from-npm-dependencies) If the `package.json` is modified, Nx uses a smart approach to understand what exactly changed. If you modify an **npm library**, Nx finds all **projects** using that **library** and marks them affected. If it is a `@types/*`, Nx extracts the **related library** and applies the same principle as if you were modifying the library. If you modify a library used in the `nx.json` plugins or delete a library, **all projects** will be considered affected: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/13-affected-npm.png) By default, modifying the **package manager lock** file affects **all projects:** ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/14-affected-lock.png) This behavior can be modified using the `projectsAffectedByDependencyUpdates` in your `nx.json`: ```JSON "pluginsConfig": { "@nx/js": { "projectsAffectedByDependencyUpdates": "auto" } } ``` Options: - `all`: Affect all projects - `auto`: Affect only projects related to the modified dependencies - `string[]`: Define a list of projects ### Step 2.5 - Find Affected Nodes From Typescript Configuration [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#step-25---find-affected-nodes-from-typescript-configuration) Modifying the global TypeScript configuration can also impact the list of affected nodes: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/15-affected-ts.png) If a **path** is modified, Nx affects the **related project** that matches the root path. However, modifying a **global config** or deleting a path affects **all projects**. ### Step 2.6 - Find Affected Nodes From Global Files [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#step-26---find-affected-nodes-from-global-files) By default, modifying `nx.json` affects **all projects**. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/16-affected-global.png) ### Step 3 - Generate Affected Graph [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#step-3---generate-affected-graph) After identifying all affected nodes, Nx **generates** an **affected graph** to determine which `nodes`, `externalNodes` and `dependencies` are **finally affected**. Nx takes the affected nodes and **recursively** searches for all **dependencies** in the **Project Graph**: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/17-generate-affected-graph.png) For example, if the affected node `lib10` is used by `lib4`, which is used by `app1`, all of these nodes will be **added** to the affected project graph. Nx applies the **same** principles to the `externalNodes`: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/18-generate-affected-graph.png) For example, if the affected npm library `enquirer` which is used by the npm library `nx` which is also used by the internal library `tools`. To be sure the Affected Graph is complete, Nx will also add the related `dependencies`. ## 🧐 Affected Investigation [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#-affected-investigation) If you still don’t understand why some projects are affected on a branch, you can always debug the Nx affected process. ### Nx Graph Visualizer [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#nx-graph-visualizer) If you open the Nx graph with the affected command, you’ll be able to see all affected projects as specified in the [🤓 Affected Reminder](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#9d37) section. You can then [explore your workspace](https://nx.dev/features/explore-graph?ref=angularspace.com#explore-your-workspace) and use multiple features like the Project Focus or the Dependency Tracker. ### Debugging [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#debugging) However, on big repositories, the graph is often hard to use for debugging. I prefer to debug the Nx-affected process to see exactly which step is responsible. You can start by putting a breakpoint in `packages/nx/src/command-line/affected/affected.ts` and run `nx show project --affected` in debug mode. ## 🤕 Affected Fixes [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#-affected-fixes) Customizing the affected process is not straightforward. If you think too many projects are affected with each modification, here are some recommendations: ### 1\. Good Splitting of Apps/Libs [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#1-good-splitting-of-appslibs) Ensure the **split of your apps/libs** is **correctly** done. Often, shared libraries are used for many projects with utilities needed by only one project. In such cases, touching that library affects all projects. ### 2\. Strict Named Inputs [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#2-strict-named-inputs) Ensure your **Named Inputs** are **correctly** configured. Named inputs define if modifying a file can impact the output of a target. For example, modifying a spec file can impact the test but not the build. If you use the default named input, touching one file affects all targets of your project. ### 3\. Affected Customization [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#3-affected-customization) Currently, there is limited customization for the affected process. You can customize it when updating a dependency by using the configuration `projectsAffectedByDependencyUpdates` (see Step 2.4 — Find Affected Nodes from Npm Dependencies). ### 4\. Patch Nx [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#4-patch-nx) > ⚠️ Only use this as a last resort! This is a **hacky** solution, but I use it to **customize** the affected process. [**Patching**](https://www.freecodecamp.org/news/git-diff-and-patch/?ref=angularspace.com) your **Nx** library using your package manager’s patch system allows you to **change** the **rules**. For example, if fixes take time to implement, you can **disable** the “**Affected All”** use cases with: ```DIFF diff --git a/node_modules/nx/src/plugins/js/project-graph/affected/npm-packages.js b/node_modules/nx/src/plugins/js/project-graph/affected/npm-packages.js index 72e78e7..7793bea 100644 --- a/node_modules/nx/src/plugins/js/project-graph/affected/npm-packages.js +++ b/node_modules/nx/src/plugins/js/project-graph/affected/npm-packages.js @@ -20,7 +20,8 @@ const getTouchedNpmPackages = (touchedFiles, _, nxJson, packageJson, projectGrap c.path.length === 2) { // A package was deleted so mark all workspace projects as touched. if (c.type === json_diff_1.JsonDiffType.Deleted) { - touched = Object.keys(projectGraph.nodes); + // PATCH TO NOT AFFECTED ALL WHEN PACKAGE IS DELETED + // touched = Object.keys(projectGraph.nodes); break; } else { diff --git a/node_modules/nx/src/plugins/js/project-graph/affected/tsconfig-json-changes.js b/node_modules/nx/src/plugins/js/project-graph/affected/tsconfig-json-changes.js index bac7008..37ae136 100644 --- a/node_modules/nx/src/plugins/js/project-graph/affected/tsconfig-json-changes.js +++ b/node_modules/nx/src/plugins/js/project-graph/affected/tsconfig-json-changes.js @@ -24,7 +24,8 @@ const getTouchedProjectsFromTsConfig = (touchedFiles, _a, _b, _c, graph) => { } // If a path is deleted, everything is touched if (change.type === json_diff_1.JsonDiffType.Deleted) { - return Object.keys(graph.nodes); + // PATCH TO NOT AFFECTED ALL WHEN PATH IS DELETED + // return Object.keys(graph.nodes); } touched.push(...getProjectsAffectedByPaths(change, Object.values(graph.nodes))); } diff --git a/node_modules/nx/src/project-graph/affected/affected-project-graph.js b/node_modules/nx/src/project-graph/affected/affected-project-graph.js index 5665c8d..d5a69aa 100644 --- a/node_modules/nx/src/project-graph/affected/affected-project-graph.js +++ b/node_modules/nx/src/project-graph/affected/affected-project-graph.js @@ -12,7 +12,8 @@ async function filterAffected(graph, touchedFiles, nxJson = (0, configuration_1. const touchedProjectLocators = [ workspace_projects_1.getTouchedProjects, workspace_projects_1.getImplicitlyTouchedProjects, - project_glob_changes_1.getTouchedProjectsFromProjectGlobChanges, + // PATCH TO NOT AFFECTED ALL WHEN PLUGIN PATTERN MATCHING CHANGED FILE + // project_glob_changes_1.getTouchedProjectsFromProjectGlobChanges, touched_projects_1.getTouchedProjects, ]; const touchedProjects = []; diff --git a/node_modules/nx/src/project-graph/affected/locators/workspace-projects.js b/node_modules/nx/src/project-graph/affected/locators/workspace-projects.js index c5aec64..edaa989 100644 --- a/node_modules/nx/src/project-graph/affected/locators/workspace-projects.js +++ b/node_modules/nx/src/project-graph/affected/locators/workspace-projects.js @@ -16,7 +16,8 @@ const getTouchedProjects = (touchedFiles, projectGraphNodes) => { exports.getTouchedProjects = getTouchedProjects; const getImplicitlyTouchedProjects = (fileChanges, projectGraphNodes, nxJson) => { const implicits = { - 'nx.json': '*', + // PATCH TO NOT AFFECTED ALL WHEN nx.json CHANGED + // 'nx.json': '*', }; Object.values(projectGraphNodes || {}).forEach((node) => { const namedInputs = { ``` ## 🙂 Last Thoughts [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/gelinjo/01-deep-dive-into-nx-affected/Deep%20Dive%20Into%20Nx%20Affected.md?ref=angularspace.com#-last-thoughts) As you can see, the Nx affected process not only considers the list of modified files but also computes the list of projects based on various other factors. This makes the investigation not always straightforward and can often lead to an affected-all situation. I hope I clarified some parts and provided you with the keys for a better understanding of the affected process. In the future, we should have more customization options for the affected process by generalizing the list of options like `projectsAffectedByDependencyUpdates`. Thanks for your attention and Stay Tuned 🚀 --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/Screenshot-2024-07-17-at-02.17.33.png) ### Host directives: decomposition unleashed! URL: https://www.angularspace.com/host-directives-decomposition-unleashed/ Last updated: 2024-08-15T08:57:40.000Z Angular 15 introduced a killer feature that is often overlooked — [Directive Composition API](https://angular.dev/guide/directives/directive-composition-api?ref=angularspace.com). It adds a new property to the Directive/Component decorator called `hostDirectives`. In it, you can list all standalone directives that you want automatically applied to your component or directive. This effectively allows you to bundle decomposed logic any way you like. It opens up many doors and I feel is quite underappreciated by the community as well as the Angular team themselves — you will only find it used ONCE in the entire components repository at the time of writing. That is probably the reason for the caveats this feature has and we will discuss them below. But not before we explore how ridiculously powerful it is! So our plan is: 1. Overview of the feature 2. Why is it important (case study: Taiga UI) 3. Examples of usage 4. Caveats and how to deal with them Let's go! ## Overview Host directives are listed as an array of directives or objects that have the directive class and all inputs/outputs that you want exposed on your host. A mental model I like to have regarding this feature is that it is something like *providers on steroids*. Previously we also could decompose some logic into services and add them to providers. Let's name key differences: 1. Providers are **lazy** — they have to be injected somewhere to be instantiated. Sometimes that's good, sometimes it's not. In that regard we can view host directives as autoinitialized providers. 2. Providers do not have access to the **host**, other than through `ElementRef`. Host directives, on the other hand, are directives, just like any other. Therefore, they can have `host` in their decorator and apply host listeners and bindings declaratively which is a big plus. 3. Providers can only be configured with DI, whereas host directives can expose **inputs**, making them much easier to work with when decomposition requires external configuration. Keeping these things in mind we see that we can achieve much more with host directives. Besides, they can just as well be used on their own in templates. I'm a huge fan of decomposition and this API provides me with tools that can substantially improve my code base. It might not be clear from the get go, but once we take a closer look at the examples I'm sure you would see how important it is. > My area of expertise lies in reusable flexible low level UI components. I'm really excited about Directive Composition API because it can help me a lot. This might not be the case if you mostly work on business logic components. ## Taiga UI Like I said at the beginning, even though this feature was introduced in Angular 15, it did not yet garner attention it deserves. None of the big libraries use it — Material, ng-zorro, PrimeNG, Bootstrap. I worked on reusable UI blocks for over 5 years and was very excited when Directive Composition API finally shipped. With this article I hope to spark more interest in Angular community, showcasing what you can do with it in a tidy, ergonomic way. The best place to go for examples that I know of is the library my team developed called [Taiga UI](https://taiga-ui.dev/?ref=angularspace.com). Recently we made a huge refactor for the next major version, where we bumped Angular to 16 and finally unlocked this feature. And boy did we run wild with it! You can search the [source code](https://github.com/taiga-family/taiga-ui?ref=angularspace.com) and see it is used about 50 times currently. So what have we learned throughout our refactor? Let's find out. ### Component is a valuable slot You can have multiple directives on one element, but only one component. Before Directive Composition API if you wanted to apply multiple directives — you would have to have a component. Imagine you need a dropdown applied to a button with some visual styles and maybe open/close logic. All of those things are coming from directives. So you would have a component: ```html ``` With something like this inside: ```html
``` What downsides does this approach have? 1. You have wasted a component slot just for composition — you do not really need this template, all it does is complicate DOM structure to apply directives. 2. It is hard to configure — you have to drill all options like dropdown content to the directives applied inside. 3. All those directives are in the view and therefore are not available to inject through DI if you want them down the line. With Angular 15+ we can just have a directive to apply everything and it will solve all the problems listed above: ```ts @Directive({ standalone: true, selector: '[customDropdown]', hostDirectives: [ VisualDirective, OpenCloseLogic, { directive: Dropdown, inputs: ['myDropdown: customDropdown'], }, ], }) export class CustomDropdown {} ``` As you can see we can even alias inputs to make it as simple to use as possible: ```html ``` ### Sidequest: directive styles Sometimes applying directives is not the only reason we have a wrapping component. One of the most requested features for Angular is to [allow directives to bundle styles](https://github.com/angular/angular/issues/17766?ref=angularspace.com). Currently we can only ship styles with components and therefore if we need something like autoprefixr, preprocessor or styles like keyframes or event just `:hover` — we must have a component since these things do not work with inline `[style]` binding. > We can also use global styles, but it is not composable, not tree-shakable and hard to ship and set up with libraries. We remedy that with a simple workaround using unencapsulated styles and dynamic components: ```ts @Component({ standalone: true, template: '', styles: '[myDir]:hover { color: red }', encapsulation: ViewEncapsulation.None, }) class MyDirStyles {} @Directive({ standalone: true, selector: '[myDir]', }) export class MyDir { protected readonly nothing = withStyles(MyDirStyles); } ``` What does `withStyles` do? It's a utility to instantiate the component which would cause styles to be added to `head`. We need a DI token that stores instantiated components: ```ts const MAP = new InjectionToken('', { factory: () => { const map = new Map(); inject(DestroyRef).onDestroy(() => map.forEach((component) => component.destroy()) ); return map; } }); ``` And a little helper to inject it and add our component when directive is created: ```ts export function withStyles(component: Type) { const map = inject(MAP); const environmentInjector = inject(EnvironmentInjector); if (!map.has(component)) { map.set(component, createComponent(component, {environmentInjector})); } } ``` Now our directives can combine logic with `hostDirectives` and apply styles using `withStyles`. With everything above addressed, time to dive into actual Directive Composition API cases. ## Examples I'll pull most of the examples directly from our library. There are many directives we apply as host directives, we will focus on these: - Appearance - Icons - Dropdown - ControlValueAccessor - Maskito ### Appearance All our components use the same fundamental directive to control their interactive appearance. The Taiga UI theme consists of declaration of CSS variables and those appearances. For example, see "Accent" [source code](https://github.com/taiga-family/taiga-ui/blob/main/projects/core/styles/theme/appearance/accent.less?ref=angularspace.com). It uses mixins for `:hover` and `:active` state so hover is not applied on touch devices and those state styles are only used on interactive elements, such as buttons or links. This way when using non-interactive badges, hovering them does not change color. Appearance directive allows you to also set states manually, for example, if you want your button with a dropdown to look pressed while the dropdown is open. This directive is applicable to buttons, chips, badges and many other components across our UI kit. It helps us reuse style declarations and behaviors, add new or custom appearances easily and then use them everywhere. For example checked checkbox uses "Primary" appearance and unchecked uses "Whiteblock", same as buttons or, potentially, even textfields: ![appearance.png](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/appearance.png) By including this directive with one line, we can enable this functionality for any component and we don't need an extra wrapping element to apply it to. [Source code](https://github.com/taiga-family/taiga-ui/blob/main/projects/core/directives/appearance/appearance.directive.ts?ref=angularspace.com) ### Icons This one is a bit more interesting. In Taiga UI 4 we moved to using CSS masks to color SVG icons with CSS. It's a pretty cool technique, because it does not even require an additional DOM element, an icon can be inside `::before`/`::after` pseudo-element. > Check out [this Stackblitz](https://stackblitz.com/edit/css-mask?ref=angularspace.com) for more interesting examples of using CSS mask! And since we do not need a DOM element — we can use a directive. Same approach with pseudo-elements can be applied to buttons, links, tabs, badges and so on. Logic that resolves the icon by its name is stored in a directive, and that directive exposes `iconStart`/`iconEnd` inputs. All we need to do is prepare our components with gaps and margins, so that when icons are added — they are properly positioned. [Source code](https://github.com/taiga-family/taiga-ui/blob/main/projects/core/directives/icons/icons.directive.ts?ref=angularspace.com) ### Dropdown Moving on from cosmetics we can take a look at our dropdowns. Previously, I already explored decomposition by dissecting dropdowns and hints in Taiga UI. I highly recommend you to read [my previous article](https://www.angularspace.com/decomposition-your-real-superpower). Host directives allow us to expand upon the ideas stated there. Basically, our dropdowns answer these questions with directives: - **What** to show? - **When** to show? - **Where** to show? And Directive Composition API is a great way to bundle those directives together. For example, we have `[(tuiDropdownOpen)]` that controls a dropdown by manually passing `true`/`false`. It is also a two-way binding, because it includes `ActiveZone` directive (which you can read about [here](https://medium.com/angularwave/tracking-user-interaction-area-6ad3ab7f0c8b?sk=aef3ab96e7aa2db73dc53c0bfe38d1c3&ref=angularspace.com)) so that it can close dropdown when we click away or navigate away with the keyboard and report it with an output. This directive in itself is a collection of host directives, but we can add it to textfields and expose both input and output to create select/combo-box/date-picker extensions to basic input. Multiple layers of host directives can be very useful! [Source code](https://github.com/taiga-family/taiga-ui/blob/main/projects/core/directives/dropdown/dropdown-open.directive.ts?ref=angularspace.com) ### ControlValueAccessor Another good case for host directives is [ControlValueAccessor](https://angular.dev/api/forms/ControlValueAccessor?ref=angularspace.com). But more generally — solving cyclic dependencies. Imagine you have an accessor that does most of what you need it for. But for some particular case you might want to check if control is touched, for example. You would need to inject `NgControl`, but that would cause cyclic dependencies, since it already injects your class as the `ControlValueAccessor`. What you can do is isolate that logic into a little directive and just add it as a host directive to your accessor. This would move it out of the way to be instantiated later when both classes are ready. This is a good example of solving a particular problem, but this approach can also be used just to break down long classes into independent pieces of code. Especially, since you can expose inputs. For example, in Taiga UI we have vertical and horizontal [Tabs](https://taiga-ui.dev/navigation/tabs?ref=angularspace.com) as 2 different components. But logic that tracks the currently active tab is reused across them as a host directive. In the [Carousel](https://taiga-ui.dev/components/carousel?ref=angularspace.com), logic that rotates slides is moved outside the main component — something that wouldn't be possible with providers because we need to be able to control the duration per slide. [InputFiles](https://taiga-ui.dev/components/input-files?ref=angularspace.com) component also moved some independent logic into a host directive — checking file type and size. It's especially useful since such a directive can implement [NG\_VALIDATORS](https://angular.dev/api/forms/NG%5FVALIDATORS?ref=angularspace.com) to improve DX when working with forms. ### Maskito [Maskito](https://maskito.dev/?ref=angularspace.com) is our framework-agnostic input masking library. A good place to learn about it would be [my overview](https://medium.com/angularwave/maskito-a-holy-grail-of-input-masking-25e729a71ef2?sk=58d97ac159ddd244b05c18053b69d7be&ref=angularspace.com). It might be really helpful for your projects! In Angular, it comes as a directive. It's easy to apply to general inputs, but sometimes you want to bundle a particular mask into a component. One such example would be inputting credit card information. We have a mask for the card number that shows it in chunks of 4 digits. We have an expiration date that would not allow you to enter the 13th month. And the CVC to only enter 3 digits. We could have just exposed those mask configs and let people apply them manually, but there are other helpful things a complete [InputCard](https://taiga-ui.dev/components/input-card?ref=angularspace.com) component can do. It can parse the payment system for you or keep chunks together in the actual form control, add a proper autocomplete attribute. That's when host directives come in handy. We can bundle Maskito with our components and configure the mask under the hood, so it's there, ready to use with one import. Same goes for many other inputs that require masking — phone numbers, dates, time or numbers. There are a lot more other examples, but I fear it might get overwhelming. If you want to learn more, you can explore Taiga UI [source code](https://github.com/search?q=repo%3Ataiga-family%2Ftaiga-ui%20hostdirectives&type=code&ref=angularspace.com). That Maskito case required us to configure a host directive from its host. And that brings us neatly to the issues section of this article. ## Caveats Working heavily with Directive Composition API I can highlight 3 main problems that let this feature down. No deal breakers though, so that's great! ### No built-in control We already saw an issue with the last example — it is hard to control inputs/props of host directives from within the host. I know this is something currently on the radar of the Angular team and they will hopefully address this issue in future. But for now we can do it ourselves using signals and a helper. > Signal inputs are not programmatically changeable, but models are! Until the Angular team adds a way to manually set signal input value, they are a hard pass for me. It's a shame, since they had transformers. First we need to inject the directive we want to control, then state a property we plan to use and provide a value for it. We might want to control it in 2 ways: imperative updates (effectively a setter) or declarative updates (effectively a getter). Signals work great here. Writable signals act as a setter, while computed would work as a getter: ```ts private readonly setter = binding(MyDir, 'prop1', initialValue); private readonly getter = binding(MyDir, 'prop2', computed(() => this.signal)); ``` That's our helper public API in a nutshell, we can now call `setter.set(value)` to update `prop1` and we can control `prop2` with other signals, used in `computed`. I have previously posted about this on X with a Stackbliz for fully typed source code, check it out: > [#AngularTip](https://twitter.com/hashtag/AngularTip?src=hash&ref%5Fsrc=twsrc%5Etfw&ref=angularspace.com) for the day! Want to be able to declaratively control inputs/public properties of \`hostDirectives\` from your components? With signals it is possible with a simple wrapper, check it out:[https://t.co/rHBbX3Il84](https://t.co/rHBbX3Il84?ref=angularspace.com) [pic.twitter.com/xAc2r92uIB](https://t.co/xAc2r92uIB?ref=angularspace.com) > > — Alex Inkin (@Waterplea) [June 3, 2024](https://twitter.com/Waterplea/status/1797675459985183218?ref%5Fsrc=twsrc%5Etfw&ref=angularspace.com) ### Manual inputs exposure I would much rather have host directives expose all inputs automatically and have concealing them an opt-in, not the other way around. This is not how the Angular team sees it, as they are worried updating some third party library version can have your components expose unexpected inputs. I don't see value in that additional safety. What I value is brevity. Problem is — you cannot just store an object with directive and inputs as a constant, since that will **not be statically analyzable**. What we can do though, is create wrapping directives. We have adopted that pattern in Taiga UI. Imagine you have a directive `A` with 3 inputs. Instead of writing it all the time as an object with an array of all those inputs, we can create one more little directive: ```ts @Directive({ standalone: true, hostDirectives: [{ directive: A, inputs: ['input1', 'input2', 'input3'], }], }) export class WithA {} ``` Now we can easily expose all those inputs with one class, adding `WithA` instead of `A` into `hostDirectives` when we need it. If you were carefully reading documentation when this feature first came out, you might remember there was a performance note warning us against using too many host directives. However it is now gone because benchmarking shows the memory/performance overhead is trivial. You can explore [this Stackblitz](https://stackblitz.com/edit/stackblitz-starters-2x1s4r?ref=angularspace.com) to stress test the Directive Composition API yourself. ### Double matching Biggest problem you would have to work around is that host directives throw an error if the same directive ends up matched twice on the same element. Frankly, I think this is something the Angular team just needs to straight up address. I firmly believe they should initialize the directive the first time we meet it and ignore all other matches, like Angular does with providers. There are some concerns regarding order of execution. It's not clear this way, which directive is created first. And directives might come in conflict when they try to bind the same property on host, for example. Regular directives are created in order of the matching attributes on the element, so you can kind of control that. I would argue that if your directives rely on the order of initialization — that's a huge anti-pattern already and it shouldn't hold Directive Composition API back. Overall, this is probably an issue I had most problems with, especially since it can come up unexpectedly through several layers of host directives. So I hope the Angular team improves that. The easiest way to run into this issue is to expose the directive and then import it again. Say you have a highlight directive that has color input: ```ts @Directive({ standalone: true, selector: '[appHighlight]', }) export class HighlightDir { // ... } ``` Imagine you expose `appHighlight` as an input when attaching it to some component, but in the same template you also want to use it somewhere else, so you import it. You end up with this directive matched twice on the same element. Once through exposed host directive input and second time by the attirbute of that input as a plain directive. So my tip to you is: **always alias exposed inputs to avoid accidental double matches.** Alternatively, don't forget that you can omit selector altogether if your directive is expected to only work as host directive. ## Summary I hope this article gave you a good overview of Directive Composition API and the power it brings to you as an Angular developer. There are limitations to this approach, as always. But everything can be dealt with, for the most part. I really like the new composition patterns it unlocked and believe people can benefit from exploring them. A quick recap for this feature in a few bullets: - `hostDirectives` allow you to automatically apply standalone directives to other directives or components - You can compose independent logical blocks together declaratively, multiple levels deep - This is a lot like providers, but with additional benefits like inputs and host bindings - It has several caveats that can be addressed with a few helpers and best practices - Check out examples in this article and their source code to get a taste of what you can do Now big complex tasks can be broken down into easily digestible pieces that come together seamlessly through this new API. And the Angular team will definitely polish it further when more people start using it and discover different ergonomics and pitfalls. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/Screenshot-2024-08-15-at-10.43.13.png) ### 10x e-Book Giveaway! - Effective Angular by Roberto Heckers URL: https://www.angularspace.com/10x-e-book-giveaway-effective-angular-by-roberto-heckers/ Last updated: 2024-08-12T02:20:37.000Z Time for another Angular Space Giveaway! Roberto Heckers - popular Angular content creator on LinkedIn released his book with PACKT. PACKT & Roberto have 10 copies of **Effective Angular** to giveaway for FREE :)! \- Kindle Price: $33.99 What's especially fun about this book is that it **features use of Nx**! In high demand these days. > By the end of this book, you’ll be able to use Angular effectively to build enterprise-ready, scalable front-end applications. Roberto would like to personally tell you all about the book and invite you to the giveaway! Checkout his video! 0:00 /0:43 1× More details about the book here: [https://www.amazon.com/Effective-Angular-Develop-applications-effectively-ebook/dp/B0D1CPXN69/ref=tmm\_kin\_swatch\_0?\_encoding=UTF8&qid=&sr=](https://www.amazon.com/Effective-Angular-Develop-applications-effectively-ebook/dp/B0D1CPXN69/ref=tmm%5Fkin%5Fswatch%5F0?%5Fencoding=UTF8&qid=&sr=&ref=angularspace.com) Instructions on how to participate in giveaway below 👇 _This post is for subscribers only._ ### [MEGA Article] - Superpowers with Directives and Dependency Injection URL: https://www.angularspace.com/mega-article-superpowers-with-directives-and-dependency-injection/ Last updated: 2024-08-13T07:52:40.000Z ## Introduction I have been saying this for a looong while: Directives are the most underutilized part of Angular. They provide a powerful toolset for doing magic in templates, and yet in most projects, it is used in the most common, "attribute directives that do some limited business logic" style. The next most underutilized thing is dependency injection. It is an awesome concept for building reusable stuff, yet 95% of DI usage in Angular is for services. I have written a bunch of articles on both topics, and here is a list of them. I recommend you read those before diving into this one, although that is not a requirement: - [Angular Dependency Injection Tips](https://medium.com/codeburst/angular-dependency-injection-tips-ddb24b8244be?ref=angularspace.com) - [Directives vs Components](https://medium.com/codeburst/directives-vs-components-8e924dd86f20?ref=angularspace.com) - [Always use "inject"](https://dev.to/this-is-angular/always-use-inject-2do4?ref=angularspace.com) In this series we will dive deeper and explore how both of those concepts can be utilized (often together) to significantly simplify our templates. We will do so on a use case examples, in a step-by-step format. > Note: I do not choose these examples specifically because they are very common or very useful; often, solutions in form of third-party libraries exist; these examples are just good from the learning perspective, as they allow to showcase a lot of concepts in a relatively small amount of code. > Disclaimer: in this article, in most cases we are going to use legacy decorator `@Input`\-s, as most developers are familiar with those. However, you can easily sibstitute them with signal inputs. One example will specifically use signal inputs to showcase how they make working with directives easier. So, without further ado, let's get started! ## Building a password strength meter A functionality that exists in lots of modern-day web apps is checking for a user's password strength. Of course, solutions for this exist in the open. but let's build something of our own, and in a way that it would be really customizable. Let's start with the simplest possible scenario: we add some class on the input element so it can be shown visually: ```typescript type PasswordStrength = 'weak' | 'medium' | 'strong'; @Directive({ selector: '[appPasswordStrength]', standalone: true, host: { '(input)': 'onInput($event)', }, }) export class PasswordStrengthDirective { private readonly el = inject(ElementRef); onInput(event: InputEvent) { const input = event.target as HTMLInputElement; const value = input.value; const strength = this.evaluatePasswordStrength(value); this.el.nativeElement.classList.add( `password-strength-${strength}` ); } evaluatePasswordStrength(password: string): PasswordStrength { if (password.length < 6) { return 'weak'; } else if (password.length < 10) { return 'medium'; } return 'strong'; } } ``` And then we can use it in the template like this: ```html ``` Fairly simple. (Ignore the simplicity of the logic behind evaluation; it is really irrelevant and we can put any logic there - our aim is to make this directive maximally customizable). But now we have several issues: 1. Why the selector? If we forget to put the `[appPasswordStrength]` attribute, the directive will not work. Can we make it work automatically on all password inputs? 2. What if we need logic that is not just adding a class, but also adding some text to the DOM, for example? Can we make the directive just inform the template about the strength of the password and then let it handle in a custom way? 3. What about customizing the evaluator function? Can we make it so that the developer (using the directive) can provide their own function to evaluate the password strength? 4. If the developer provides the logic for evaluation, can we make it possible to both provide the logic application-wide, from one place, and customize it on a per-input basis? Let's explore all of these issues and improve our directive. Let's start with the first, simplest one: ```typescript @Directive({ selector: 'input[type="password"]', standalone: true, }) // directive implementation ``` Now we can just drop the attribute selector: ```html ``` Now it will work automatically. But what if, in some cases, we want to ignore the checking? We can add an input for that: ```typescript @Directive({ selector: 'input[type="password"]', standalone: true, host: { '(input)': 'onInput($event)', }, }) export class PasswordStrengthDirective { @Input() noStrengthCheck = false; private readonly el: inject(ElementRef); onInput(event: InputEvent) { if (this.noStrengthCheck) { return; } // logic goes here } // the other methods } ``` And then we can use it like this: ```html ``` Cool, the first improvement is done. Let's now make it so the component, rather than add a class, just informs the template about the strength of the password and lets it do the job itself. We *could* do that by adding an output, but that would mean more boilerplate for the developers in the template to capture the strength in a variable before using it. So instead we will use `exportAs` to work with the directive instance directly: ```typescript @Directive({ selector: 'input[type="password"]', standalone: true, exportAs: 'passwordStrength', host: { '(input)': 'onInput($event)', }, }) export class PasswordStrengthDirective { @Input() noStrengthCheck = false; // property to capture in the template strength: PasswordStrength = 'weak'; // no need for ElementRef anymore onInput(event: InputEvent) { if (this.noStrengthCheck) { return; } this.strength = this.evaluatePasswordStrength(value); } evaluatePasswordStrength(password: string): PasswordStrength { if (password.length < 6) { return 'weak'; } else if (password.length < 10) { return 'medium'; } return 'strong'; } } ``` Now we only write the strength itself to a property to let the developer capture it in the template. Here is how it is done: ```html @switch (evaluator.strength) { @case ('weak') {
Weak password
} @case ('medium') {
Medium password
} @case ('strong') {
Strong password
} } ``` We use `exportAs` to capture the directive instance in a template variable, and then we can use it to access the strength property. You can read more about it in [the official documentation](https://angular.dev/api/core/Directive?ref=angularspace.com#exportAs). Now, let's make it so the developer can provide their own logic for evaluating the password strength. Again, we *could* do it using a standard `Input` property, but that would mean we would have to provide that function every time we have a password input, and that is cumbersome and error-prone - easy to forget. So instead we will use an `InjectionToken` together with a small helper function to provide the logic application-wide: ```typescript type PasswordEvaluatorFn = (password: string) => PasswordStrength; export const EVALUATOR_FN_TOKEN = new InjectionToken< PasswordEvaluatorFn >( 'PasswordEvaluatorFn', ); export function providePasswordEvaluatorFn( evaluatorFn: PasswordEvaluatorFn, ) { return [{ provide: evaluatorFnToken, useValue: evaluatorFn, }]; } @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector selector: 'input[type="password"]', exportAs: 'passwordEvaluator', standalone: true, host: { '(input)': 'onInput($event)', }, }) export class PasswordEvaluatorDirective { strength: PasswordStrength = 'weak'; @Input() evaluatorFn = inject(evaluatorFnToken); @Input() noStrengthCheck = false; onInput(event: InputEvent) { if (this.noStrengthCheck) { return; } const input = event.target as HTMLInputElement; const value = input.value; this.strength = this.evaluatorFn(value); } } ``` Now we can just provide a custom evaluation function application-wide: ```typescript export function customPasswordEvaluator(password: string) { if (password.length < 6) { return 'weak'; } else if (password.length < 10) { return 'medium'; } return 'strong'; } bootstrapApplication(AppComponent, { providers: [ providePasswordEvaluatorFn(customPasswordEvaluator), ], // the rest of the application }); ``` And use it as we please. But here comes a problem: what if the user does not provide a custom evaluator function? We can make it so the directive throws an error if it is not provided, but that might not be the best solution. So, let's instead make the directive use a default evaluator function if the user has not provided one. But, right now, if there is no custom function provided, the dependency injection mechanism will throw a `NullInjectorError` error. Here, the `optional` flag comes to the rescue: ```typescript @Directive({ //... }) export class PasswordEvaluatorDirective { //... evaluatorFn = inject(evaluatorFnToken, { optional: true }); //... } ``` Now the `inject` function will return `null` instead of throwing an error if the token is not provided. We can use that to provide a default evaluator function: ```typescript export const defaultEvaluatorFn: PasswordEvaluatorFn = ( password: string, ): PasswordStrength => { if (password.length < 6) { return 'weak'; } else if (password.length < 10) { return 'medium'; } return 'strong'; } @Directive({ //... }) export class PasswordEvaluatorDirective { //... evaluatorFn = inject( evaluatorFnToken, { optional: true }, ) ?? defaultEvaluatorFn; //... } ``` So now, if the user is satisfied with the default evaluator function, they don't have to provide anything. But if they want to provide their own, they can do that as well, both at the component level *and* application-wide. So now, that last question remains: how to provide a custom evaluator on an input basis? Meaning, we can have several password inputs in the same component, but we want some of them to work differently. Because of how the `inject` function works, we can just decorate our `evaluatorFn` with `@Input` and it will work: ```typescript @Directive({ //... }) export class PasswordEvaluatorDirective { //... @Input() evaluatorFn = inject( evaluatorFnToken, { optional: true }, ) ?? defaultEvaluatorFn; //... } ``` And now we can use it like this: ```html ``` Here is the final version of our component, with a live demo: ## What's next? In this part, we have explored how to use `InjectionToken` to provide custom logic to a directive, how to use an exported instance of the directive, and use a custom selector for matching. In the next one, we will dive into using structural directives and performing advanced DOM manipulations. We are going to explore structural directives and how we can make components and directives interoperate and reduce clutter in our `.html` files even further. Let's build a loader component! ## The Use Case Imagine the following scenario: we have a component that loads some data, and we want to show a loading indicator while the data is being fetched. The following criteria should be met: 1. We should be able to wrap any template inside our loader component, and it will display a spinner when needed 2. The component should receive an input property indicating whether the data is being loaded or not 3. The template should be covered by an overlay, so that the user cannot interact (and potentially trigger other HTTP calls) while the data is being loaded Here is a pretty simple implementation: ```typescript @Component({ selector: 'app-loader', template: `
@if (loading) {
}
`, standalone: true, styles: [ ` .loading-container { position: relative; } .blocker { background-color: black; position: absolute; top: 0; z-index: 9999; width: 100%; height: 100%; opacity: 0.4; } `, ], imports: [NgIf, ProgressSpinnerModule], }) export class LoaderComponent { @Input() loading = false; } ``` > *Note*: I am using [PrimeNG](https://primeng.org/?ref=angularspace.com) for the examples in this article, but you can easily reuse them with any other implementation So, here we just project any content that we receive into `ng-content` and the rest is some simple CSS + PrimeNG `ProgressSpinner` component. The `loading` input property is used to toggle the spinner on and off. Now we can use it in the template as follows: ```html

Some content

``` > Wait, I thought this article is about directives? Well, good news: it *is* about directives. But what's the problem with the component? Well, the example of its usage we saw was quite optimistic: real-life scenarios usually are not that simple. Consider this piece of template: ```html

Some content

Some other content

Even more content

``` Now, here, when we have some nested elements, and the template keeps unnecessarily growing, adding more indentation levels, more closing tags, and so on, and so on. What I personally would really love to be able to do is the following: ```html

Some content

``` But how can we achieve this? Well, we need a directive that does the following things: 1. Creates a `LoaderComponent` instance dynamically 2. Somehow projects the nested template into it 3. Keeps them in sync - when the `loading` input property changes, the directive should update the `LoaderComponent` instance accordingly 4. Render the whole stuff Let's dive into it! ## Structural Directives [Structural directives](https://angular.dev/guide/directives/structural-directives?ref=angularspace.com) are really cool, because they allow us to reference some templates via [TemplateRef](https://angular.dev/api/core/TemplateRef?ref=angularspace.com) and do all sorts of magic with it. Also, we can use the [ViewContainerRef](https://angular.dev/api/core/ViewContainerRef?ref=angularspace.com) to create components dynamically. What will be left for us is to project the template into the component, And yes, *this is possible!* Let's start simple: ```typescript @Directive({ selector: '[appLoading]', standalone: true, }) export class LoaderDirective { private readonly templateRef = inject(TemplateRef); private readonly vcRef = inject(ViewContainerRef); @Input() appLoading = false; templateView: EmbeddedViewRef; loaderRef: ComponentRef; } ``` Here we injected the things we need (`TemplateRef` and `ViewContainerRef`), added a `loading` input, naturally, and created two properties: `templateView` and `loaderRef`. The first one will be used to store the reference to the template that we get, and the second one will be used to store the reference to the `ComponentRef` instance that we are going to create - we have to store both. Next, let's do some initial heavy lifting to set the whole thing up: ```typescript @Directive({ selector: '[appLoading]', standalone: true, }) export class LoaderDirective implements OnInit { private readonly templateRef = inject(TemplateRef); private readonly vcRef = inject(ViewContainerRef); @Input() appLoading = false; templateView: EmbeddedViewRef; loaderRef: ComponentRef; ngOnInit() { this.templateView = this.templateRef.createEmbeddedView({}); this.loaderRef = this.vcRef.createComponent(LoaderComponent, { injector: this.vcRef.injector, projectableNodes: [this.templateView.rootNodes], }); this.loaderRef.setInput('loading', this.appLoading); } } ``` Here, our `ngOnInit` lifecycle method does four things: 1. Make the template into an embedded view, so we can render it dynamically 2. Create a `LoaderComponent` instance 3. Project the template into the `LoaderComponent` instance via `projectableNodes` \- here is where the magic is happening! 4. Set the `loading` input property on the `LoaderComponent` instance Now, this way it will kinda work, but we need two more things to make it work properly: 1. We need to update the `LoaderComponent` instance when the `loading` input property changes 2. We need to ensure change detection still works on the projected template despite it being "detached" from the parent view and projected into a new component. We will use `ngDoCheck` for this Let's finalize the implementation: ```typescript @Directive({ selector: '[appLoading]', standalone: true, }) export class LoaderDirective implements OnInit, DoCheck, OnChanges { private readonly templateRef = inject(TemplateRef); private readonly vcRef = inject(ViewContainerRef); @Input() appLoading = false; templateView: EmbeddedViewRef; loaderRef: ComponentRef; ngOnInit() { this.templateView = this.templateRef.createEmbeddedView({}); this.loaderRef = this.vcRef.createComponent(LoaderComponent, { injector: this.vcRef.injector, projectableNodes: [this.templateView.rootNodes], }); this.loaderRef.setInput('loading', this.appLoading); } ngOnChanges() { this.loaderRef?.setInput('loading', this.appLoading); } ngDoCheck() { this.templateView?.detectChanges(); } } ``` Those additions are fairly simple: when the `loading` property of the component changes, we update the `LoaderComponent` instance accordingly, and when the change detection runs for the directive instance, we also notify the child template in the `ngDoCheck` lifecycle method via `templateView.detectChanges()`. This is to avoid issues when a child component inside the template view uses the `OnPush` strategy. If you are unfamiliar with how `ngDoCheck` works or why it is used, you can read [the official docs](https://angular.dev/api/core/DoCheck?ref=angularspace.com), or [this tutorial](https://www.tektutorialshub.com/angular/angular-ngdocheck-life-cycle-hook/?ref=angularspace.com). Now we can simply use it in the template, even when we have multiple nested elements: ```html

Some content Some other content

Even more content

``` And so, there is no nested template, no unnecessary indentation, and no unnecessary closing tags. It's just a simple directive that does the job. You can view the full example with a live demo on StackBlitz: ## Where do we go next? As mentioned previously, I believe directives are very, **very** powerful, but sadly underused in the wider community. In the next part, we will explore using directives to hack into existing components. In previous parts, we explored how to use directives to maintain and export local state in templates and reuse it, and how to use structural directives to simplify our templates. In this part, we are going to explore how we can use directives to hijack existing elements and components to add or modify functionality. So, let's explore use cases. ## Use case 1: Hijacking existing elements Imagine the following scenario - we are maintaining a website that displays lots of content, and also has lots of internal pages. That content often contains links to external resources, and also obviously links for internal navigation between pages. Now, imagine, we want to (almost) always open external links in a new tab (for example, not to disrupt the user's reading flow and general UX). So, obviously, the simplest solution would be to just add `target="_blank"` to all external links. This is okay, but obviously is a bit tedious, and also will require the developers always to remember to do that and pass this knowledge to future team members. What if we could automate it? So, naturally, we need a directive that will 1. Bind to all `a` elements 2. Determine if the link in its `href` attribute is external 3. Add `target="_blank"` to the element if it is 4. Possibly add a way to exclude some links from this behavior 5. Also keep in mind that some links can change dynamically So, let's first examine how we can determine if a link is external or not. It can be done by several fairly simple functions: ```typescript function getHostFromUrl (url: string) { return new URL(url).hostname.replace("www.", ""); }; function isAbsoluteUrl (url: string) { const formatedUrl = url.toLowerCase(); return formatedUrl.startsWith("http") || formatedUrl.startsWith("https"); }; function isUrlExternal (url: string, host = window.location.hostname) { if (isAbsoluteUrl(url)) { const providedHost = getHostFromUrl(url); return providedHost !== host; } else { return false; } }; ``` We are leveraging the [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL?ref=angularspace.com) constructor to parse the URL and check its origin. Next, let's build a simple directive that can add `target="_blank"` to external links. This directive will use signal inputs to keep track of both the current target attribute and the `href` attribute: ```typescript type Target = '_blank' | '_self' | '_parent' | '_top' | ''; @Directive({ selector: 'a:not([noBlank])', standalone: true, host: { '[target]': 'target()', '[href]': 'href()', }, }) export class ExternalLinkDirective { targetRef = input(''); href = input.required(); target = computed( () => isUrlExternal(this.href()) ? '_blank' : this.targetRef() ); } ``` Now, this directive is very simple (just 14 lines of code), and does *everything* we want from it; we take the `href`, we figure out if the link is external or not, and we compute the `target` attribute to put on the element. We also utilize a trick to be able to sometimes avoid this behavior. The `:not()` selector is a css selector supported by Angular Directive's API-s which allows the exclusion of some elements. Here we exclude all elements with `noBlank` attribute. Now, we can just add `noBlank` to the links we don't want to open in a new tab: ```html Google Google ``` Here is a full working example of this directive with a preview: ## Use case 2: hijacking existing components This part is completely inspired by a blog post written by [Tim Deschryver](https://timdeschryver.dev/blog/use-angular-directives-to-extend-components-that-you-dont-own?ref=angularspace.com), where he discusses how to use directives to extend functionality on components that we didn;t write. In that example, a directive is used to hijack the functionality of a `Calendar` component from the PrimeNG UI library. Here is the example from this article: ```typescript import { Directive } from '@angular/core'; import { Calendar } from 'primeng/calendar'; @Directive({ selector: 'p-calendar', }) export class CalenderDirective { constructor(private calendar: Calendar) { this.calendar.dateFormat = 'dd/mm/yy'; this.calendar.showIcon = true; this.calendar.showButtonBar = true; this.calendar.monthNavigator = true; this.calendar.yearNavigator = true; this.calendar.yearRange = '1900:2050'; this.calendar.firstDayOfWeek = 1; } } ``` Meaning we do not need to provide default inputs when working with the `p-calendar` component, as they are already set by the directive. This is a great example of how to use directives to extend the functionality of existing components. > Give a read to Tim's article, it has lots of other very interesting use cases The same approach can be used to modify existing components that we wrote in our application. ## Going forward As we have seen, directives are also powerful when dealing with existing functionality, and can be used to extend it, thus avoiding the need to wrap everything in components. In the next part, we are going to explore how directives can be used to work with events - both custom and native. We have explored directive usage in template-local logic, structural directives instead of components, and using directives to extend the functionality of existing components and/or elements. This time around, we are going to find out how directives can be used to work with events and add events to components that do not *really* exist. Let's get started with two interesting use cases! ## A click-away directive Sometimes we need to know when the user clicked outside of a given element. This is a common use case for dropdowns, modals, and other components that need to be closed when the user clicks outside of them. This can also be useful in a gaming app, or an app that shows videos (clicking away pauses the video, etc). We could do something inside of the component that has this functionality, but that would not be a very reusable piece of logic, considering we might need something like that in other components too. So, let's build a directive for this! It is going to: - take the target element - inject the `Renderer2` instance to be able to listen to events - listen to all click events on the document element - if the target is not a descendant of the clicked element (thus being outside of it), emit an event - dispose of the event listener when the directive is destroyed Here is our implementation: ```typescript @Directive({ selector: '[clickAway]', standalone: true, host: { '(window:click)': 'handleKeyDown($event)', } }) export class ClickOutsideDirective { private readonly elRef: ElementRef = inject( ElementRef, ); @Output() clickAway = new EventEmitter(); handleKeyDown(event: KeyboardEvent) { if (!this.elRef.nativeElement.contains(event.target as HTMLElement)) { this.clickAway.emit(); } } } ``` As you can see, the logic is very straightforward. We inject the `ElementRef` instance to get the target element, then use `host` to be able to listen to `window` events. We then listen to all click events on the window, and if the target is not a descendant of the clicked element (we check it using the [Node.contains](https://developer.mozilla.org/en-US/docs/Web/API/Node/contains?ref=angularspace.com) method), we emit a `clickAway` event. We then dispose of the event listener in `ngOnDestroy`. Now, the cool thing about naming the `EventEmitter` the same as the directive selector is that we can just add this custom event on any element we like: ```html

Click outside of me!

``` Works like magic, as if it were a native event like `(click)` or `(mouseover)`! Here is a working example with a preview: Now, on to our next example. ## Handling scrolling Now, an often guest in different web apps is the ability to perform actions (like loading more content) when certain elements become visible in the view. This is a common use case for infinite scrolling, lazy loading, and other similar features. Again, as in the previous example, let us explore solutions that would allow us to reuse this logic in multiple places. Essentially, what we want is to have a custom event that would fire as soon as the element enters the viewport. We are going to use an [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection%5FObserver%5FAPI?ref=angularspace.com) for this, which allows observing whether a given element is intersecting with (essentially being visible inside) another element or the entire viewport. This will be a simplified example (lots of nuances can be present based on how exactly we want this to work), but the basic is that we can - take a target element - listen to all of its intersection changes - If it is interesecting with the viewport, emit an event Now, here is an implementation: ```typescript @Directive({ selector: '[scrollIntoView]', standalone: true, }) export class ScrollIntoViewDirective implements OnInit, OnDestroy { @Input() threshold = 0.25; @Output() scrollIntoView = new EventEmitter(); elRef = inject(ElementRef); observer: IntersectionObserver; ngOnInit() { this.observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { this.scrollIntoView.emit(); } }); }, { threshold: this.threshold } ); this.observer.observe(this.elRef.nativeElement); } ngOnDestroy() { this.observer.disconnect(); } } ``` Here, we create an `IntersectionObserver` instance, and we listen to all of its changes. If the target element is intersecting with the viewport, we emit a `scrollIntoView` event. We then disconnect the observer in `ngOnDestroy`. As you can see, the implementation is fairly similar to what we did with the `ClickAway` directive, the difference being the "business logic". Here is how we can use it in a template: ```html
Really long content goes here
Dynamic content goes here
``` Again, works like magic as any other native event! Here is the preview: > Note: the example shows a long list of `div`\-s, scroll to the bottom to see a text message being logged into the console. ## Next steps As our exploration goes on, we learn more and more interesting use cases for Angular directives. In the next part, we will learn how to show templates outside of our components using directives and the concept of Portals. In previous parts, we have already explored various applications of directives and dependency injection. This time around we shall see how we can use directives to help components communicate on the most hard-to-reuse level - the template. So, let's get started with the next use case. ## Dynamic shared templates Imagine a fairly standard application UI structure: we have a header, a footer, maybe a sidebar, and some content in a `main` tag. The header component is of high interest to us, because while it is the same for all pages, for certain pages it might require the addition of some custom templates. For example, if we are on the "Order Details" page, it may display the relevant product list, while on the "Shopping Cart" page it may display the cart summary. In other words, we need to be able to dynamically add some content to the header. A relatively naive thing to do would be to subscribe in some way to the router and change the header template accordingly. But this has a couple of downsides: 1. Header component will become bloated 2. There won't be a clear way for components to communicate data to the header for their related pieces of the template 3. We might need this sort of solution for other pages, meaning more bloat What if we could just create the template in the component itself, and then somehow tell it to display that content in the header instead of its own template? Turns out, this is entirely possible! Let's see how ## The Idea For this example, we are going to use [Angular Material](https://material.angular.io/?ref=angularspace.com), and specifically, its [Portals](https://material.angular.io/cdk/portal/overview?ref=angularspace.com) feature. Portals come from the `@angular/cdk` package and allow us to render a template outside of its original context. In our case, we will use them to render a template in the header component. > Note: this could be done without portals, or, anyway, without the `@angular/cdk` package, but this approach would simplify a couple of things. You are welcome to try this out with just `ng-template`\-s So, what is the general idea behind our solution? Three things 1. An `ng-template` in the header in the correct place where want the dynamic content to be rendered, with the portal directive added to it 2. Our own custom directive that will capture a template from any other component 3. A service that would communicate from the directive instance to any component (the header in our particular place) that wants to use the template Let's start with the service, that actually shares the portal between consumers: ## The Implementation ### The Service ```typescript @Injectable({providedIn: 'root'}) export class PortalService { private readonly portal$ = new Subject< {portal: Portal | null, name: string} >(); sendPortal(name: string, portal: Portal | null) { this.portal$.next({portal, name}); } getPortal(name: string) { return this.portal$.pipe( filter(portalRef => portalRef.name === name), map(portalRef => portalRef.portal), ); } } ``` Let's understand what goes on here. First of all, we have the `portal$` subject, which will take an object that describes the portal; it will receive a name (where we want to show the template, say, `header`), and the portal itself. The `sendPortal` method is used to send the portal to the service so that subscribers can use it, and the `getPortal` method is used to get a particular portal from the service. The `getPortal` method is quite simple, but it makes the service (and directive that will use it) very reusable so that we can send different templates to different places throughout the application. So now, that we have the service, let's create the header component and use this service to display the content: ### The Header Component ```typescript @Component({ selector: 'app-header', standalone: true, template: ` Header `, imports: [MatToolbarModule, PortalModule, AsyncPipe], }) export class HeaderComponent { private readonly portalService = inject(PortalService); portal$ = this.portalService.getPortal('header'); } ``` As you can see, the component selects its specific portal template via our service, then uses the [cdkPortalOutlet](https://material.angular.io/cdk/portal/api?ref=angularspace.com#PortalOutlet) directive to render it. We then use the `async` pipe to subscribe to the portal observable and render the template when it is available. (*note*: if we pass `null` to `cdkPortalOutlet`, it will render nothing, that is going to be important in the directive). As now we have ourselves on the receiving side of things, we can go on and create the directive that does the heavy lifting. ### The Directive As we are going to work with templates, the directive will be a structural one. We will call it `portal`, and it will take an input with the same name, which will be the name of the portal we want to send the template to. ```typescript @Directive({ selector: "[portal]", standalone: true, }) export class PortalDirective implements AfterViewInit, OnDestroy { private readonly templateRef = inject(TemplateRef); private readonly vcRef = inject(ViewContainerRef); private readonly portalService = inject(PortalService); @Input() portal!: string; ngAfterViewInit() { const portalRef = new TemplatePortal( this.templateRef, this.vcRef, ); this.portalService.sendPortal(this.portal, portalRef); } ngOnDestroy() { this.portalService.sendPortal(this.portal, null); } } ``` As you can see, we inject both `TemplateRef` and `ViewContainerRef` to create a `TemplatePortal` instance, which we then send to the service in the `ngAfterViewInit` lifecycle hook. Actually, we do not do any manipulations on the portal, or the template, we delegate it all to the `TemplatePortal` constructor. On `ngOnDestroy`, we send `null` to the service, so that the header component will remove the now obsolete template. Now, we can try this in action: ### The Usage ```typescript @Component({ selector: 'app-some-page', standalone: true, template: `
Custom header content Some content
`, imports: [PortalDirective], }) export class SomePageComponent {} ``` So in this example, the "Custom header content" text will not be rendered in this component, but rather, in the header component. Notice we did **not** import the `HeaderComponent`, we did not put it in the template of the `SomePageComponent`, or do anything else boilerplate-ish, we just dropped the `portal` directive on some template, and that's it. Another cool aspect of this is that the template that was "teleported" is still "owned" by the component in which it was written, meaning data bindings work as expected so that we can have dynamically changing data "portal-ed" somewhere else, like this: ```typescript @Component({ selector: 'app-some-page', standalone: true, template: `
{{someData}}
`, imports: [PortalDirective], }) export class SomePageComponent { someData = 'Custom header content'; changeContent() { this.someData = 'New content'; } } ``` Now, if we go on and click on the button, the header will change its content to "New content". You can view this example in action here: Click on the links to navigate from one page to another, and notice how the content in the header is changed dynamically ## Final tasks This time, we explored a more specific use case for an Angular directive. Directives, as mentioned multiple times throughout this series, are a very powerful tool, and one that is criminally underused. Next, we will cover the most controversial aspect of directives, which is how we can use them to bring business logic into our templates. First of all, let's reflect on what we have already done, and understand how the next cases are fundamentally different. In previous articles, we created directives that did some specific, reusable, and very useful things. For example, we built a directive that - checked for password strength - emitted custom events when the host element scrolled into view - transported a template view into another component - displayed a loader - added some default inputs onto existing components As you can see, there is a common theme for all these cases - all those directives are super-reusable in the sense that they can be dropped in any project and just work; none of them really contain any business logic. But in a lot of cases, with real-life projects, it is the business logic that we want to be reusable and easily shareable. So what scenarios are there? ## Using directives to handle permissions Imagine that our app has permissions to enable or disable certain features. For example, we might have a feature that allows users to edit their profile, but only if they have the right permissions. We have a few `PermissionService` that has a `hasPermission` method that takes a permission name and returns a boolean. We could use this service in our component like this: ```typescript @Component({ selector: 'app-profile', template: ` @if (hasPermission('edit-profile')) {
} ` }) export class ProfileComponent { constructor(private permissionService: PermissionService) {} hasPermission(permissionName: string) { return this.permissionService.hasPermission(permissionName); } editProfile() { // ... } } ``` But now every time we want to check for this or that permission, we need to inject the service, maybe write a wrapper method, call it from the template, or some other mildly cumbersome way. Also, if the way how we access those permissions changes (for example, the service now has a different API interface), we might face a pretty large refactoring. Also, if were have some sort of state management solution that works with Observables (like NgRx, for instance), our code will become even more complex. So how do we deal with this? Well, wouldn't it be nice if we could just do this: ```typescript @Component({ selector: 'app-profile', template: `
` }) export class ProfileComponent { editProfile() { // ... } } ``` So now we can pass the permission name as an input to the directive, and the directive will handle the rest. Let's see how we can do this. - First, we need to create a directive that injects the `PermissionService` and has an input property that will accept the permission name - next, we will add `ngIf` as a HostDirective so it can handle showing/hiding the template - and finally, we will inject the reference to the `NgIf` directive, and pass the resulting boolean to it Let's do this: ```typescript @Directive({ selector: '[hasPermission]', standalone: true, hostDirectives: [NgIf], }) export class HasPermissionDirective { private readonly permissionService = inject(PermissionService); private readonly ngIfRef = inject(NgIf); @Input() set hasPermission(permissionName: string) { // we can use any other approach here this.ngIfRef.ngIf = this.permissionService.hasPermission( permissionName, ); } } ``` Now, our directive works *almost* perfectly. What do we need to add? Of course, a possibility to handle the case when the user does not have the permission. We can do this by adding an `else` template to our directive: ```typescript @Directive({ selector: '[hasPermission]', standalone: true, hostDirectives: [NgIf], }) export class HasPermissionDirective { private readonly permissionService = inject(PermissionService); private readonly ngIfRef = inject(NgIf); @Input() set hasPermission(permissionName: string) { this.ngIfRef.ngIf = this.permissionService.hasPermission( permissionName, ); } // the improtant part @Input() set hasPermissionElse(template: TemplateRef) { this.ngIfRef.ngIfElse = template; } } ``` Now, we essentially just do the business logic and pass the result to the `NgIf` directive. Here is a working example with a preview: ## Using directives to handle shared data in components Imagine we use some UI library that allows us to show dropdowns: ```typescript @Component({ selector: 'app-profile', template: `
` }) export class ProfileComponent { dropdownItems = [ { label: 'Item 1', value: 1 }, { label: 'Item 2', value: 2 } ]; } ``` And in some cases, we need to pass the same list of options to multiple dropdowns. For example, the same list of permissions we encountered earlier in this article could be really useful in lots of places: ```typescript @Component({ selector: 'app-profile', template: `
` }) export class SomeComponent { permissionService = inject(PermissionService); permissions = this.permissionService.getPermissions(); } ``` And then we could really see this same code in multiple places throughout our app. So how can we make this more reusable? Well, we could create a component that will wrap the dropdown and accept the list of items as an input: ```html ``` But that would introduce a set of problems: - CSS encapsulation - Having to pass eventually all of the third-party dropdown's inputs and outputs up and down - More complexity in our templates So what can we do instead? Well, we can create a directive that will inject the list of items into the third-party dropdown: ```typescript @Directive({ selector: 'third-party-dropdown[permissionList]', standalone: true, }) export class PermissionsDropdownDirective implements OnInit { thirdPartyDropdown = inject(ThirdPartyDropdown); permissionService = inject(PermissionService); ngOnInit() { this.thirdPartyDropdown.items = this.permissionService.getPermissions(); } } ``` And now we can use it like this: ```html ``` And that's it! Now our template is simple, the `third-party-dropdown` is still as it used to be, and we got additional reusable functionality for it. Here is the example with a preview: ## Last stop We have explored a huge amount of possibilities that directives give us. While there are plenty more, this much is fairly enough for projects of different sizes, so this article series is nearing its end. In the next and last part, we will talk in detail about directive selectors, what we can use, how can we combine different selectors, and what are some pitfalls or where we *cannot* use a certain selector. ## Directive Selectors All Angular directives have a selector, which specifies which HTML elements will the directive work with. Most often (in my experience, around 95% of scenarios), the selector is an attribute selector, which means that the directive will essentially be a custom HTML attribute. However, in previous articles, we saw that directives can be much more than just custom attributes. So, let us finish this series by exploring all of the other selector types, how they can be used, and some possible pitfalls, all with real-life, useful examples. ### Non-custom attribute selectors In this scenario, instead of inventing a new attribute, we use an existing, valid HTML attribute. This is useful when we want to extend the functionality of an existing HTML element, without having to create a new attribute for it. For instance, we might put ids on our HTML elements to mark things that are needed for E2E testing or something like that. However, some E2E test runners might be using different attributes, like `data-test-id` or `data-qa-id`. Instead of copy-pasting the same id attribute with a different value, we can create a directive that will add the attribute we need based on the `id` attribute: ```typescript @Directive({ selector: '[id]', standalone: true, host: { 'attr.data-qa-id': 'id', 'attr.data-test-id': 'id', }, }) export class TestingIdDirective { private readonly elRef = inject>(ElementRef); get id() { return this.elRef.nativeElement.id; } } ``` In this example, we just pick any element that has an `id` property, and then set its value unto two new attributes `data-qa-id` and `data-test-id` using [Host metadata](https://angular.dev/guide/components/host-elements?ref=angularspace.com#binding-to-the-host-element). In the end, it does not matter what type of element we are working with, as long as it has an `id` property, we can use this directive on it. You can find a working reproduction of this example [here](https://stackblitz.com/edit/stackblitz-starters-kc9dwb?file=src%2Fmain.ts&ref=angularspace.com). Of course, this example is very broad, and that is why we have to discuss our next, more narrow one. ### Attribute selectors with values As we saw, the previous example targeted elements that just had an `id` attribute, without much regard as to what that particular `id` is. However, there can be other motives for us to use an attribute selector. Let's consider the following example: we have many inputs of type `number`, but in our application, we only want to allow positive numbers. If the user inputs a negative number, we want to outline the input with the color red. We have a CSS style that does exactly that when encountering a `.invalid` class, so, what is left to us is to target inputs of type `number`, check their value, and add or remove the `.invalid` class based on that. We can do that with the following directive: ```typescript @Directive({ selector: 'input[type="number"]', standalone: true, host: { 'class.invalid': 'value', '(input)': 'onInput()', }, }) export class PositiveNumberDirective { private readonly elRef = inject>(ElementRef); value = false; onInput() { const value = this.elRef.nativeElement.value ?? 0; this.value = (+value) < 0; } } ``` Again, we do not have to do much here other than importing the directive into the component we want to use it with, as it will automatically bind to the inputs of type `number` and add the `.invalid` class to them if the value is negative, which we will know because of the `HostBinding`. You can find a working reproduction of this example [here](https://stackblitz.com/edit/stackblitz-starters-bt4p7b?file=src%2Fmain.ts&ref=angularspace.com). Next, let us discuss an often-overlooked use case of directive selectors. ### Class selectors Yes, you might have not heard about them, but directives can actually bind to CSS classes too! Consider the following scenario: in the previous example, we added the `.invalid` class to the input, but now we want to add some logic to all the `.invalid` elements in the application. For example, we might want to add a tooltip to them, which is also accomplished with a separate directive that already exists. We can achieve this with the power of Host Directives combined with a class selector: ```typescript @Directive({ selector: '.invalid', standalone: true, hostDirectives: [TooltipDirective], }) export class InvalidTooltipDirective implements AfterViewInit { private readonly tooltipRef = inject(TooltipDirective); ngAfterViewInit() { this.tooltipRef.title = 'The value is invalid'; } } ``` This will *kinda* achieve what we want, however has a glaring problem, which we will discuss shortly after we cover all the selector types. At the moment, let's focus on our next level of knowledge about directive selectors - that is way more complex and versatile ones. ## Complex selectors So far, we discussed selectors that specifically target some HTML elements. This is handy, but there are scenarios where we might want to target more than one type of element, or maybe exclude *some* elements from an otherwise too broad selector. Let's see how we can achieve that. ### Combining selectors There are two ways of combining selectors to either broaden the "target audience", so to speak, of a directive: specifying a more concrete attribute or using multiple selectors. We already discussed the first scenario (the `input[type="number"]` in the `PositiveNumberDirective` two examples prior), so let us just talk about using multiple selectors. Let's say we have a social media application built with Angular, where users can interact and talk to each other. Each user has an online status, which is determined by a number of things (just having a page open is not enough to consider a user to be "online" at the moment). One of those things is whether the user is typing something. There are a number of places where a user can type, for example, `input`\-s and `textarea`\-s. We want a directive that will capture that event and send a quick HTTP call to the server notifying that the user is currently online (there are a bunch of optimizations we can do about this, for instance, checking if the user is not already marked as "online", and making sure we do not send an HTTP call on each keystroke, but we will skip them for the sake of brevity, as those are out of the scope of this article). Let's build such a directive: ```typescript @Directive({ selector: 'input, textarea', standalone: true, host: { '(input)': 'onInput()', }, }) export class OnlineStatusDirective { private readonly userService = inject(UserService); onInput() { this.userService.setOnlineStatus(true); } } ``` And again, as with other directives, just importing it into the component we want to use it with will be enough to make it work and target multiple types of elements. You can find a working reproduction of this example [here](https://stackblitz.com/edit/stackblitz-starters-um9ci5?file=src%2Fmain.ts&ref=angularspace.com). Let's now move on to our final entry, which helps narrow down the selector's specificity instead of broadening it. ## Selector negation Sometimes, we write a selector that targets all the elements that we want but encounters this one element that it just shouldn't apply to, despite matching. For instance, in the second example in this article, the `PositiveNumberDirective`, targeted *all* the inputs of type `number`, but we might have a specific input that we do not want to apply this directive to. Our first thought might be to add an Input property to signify if the directive should actually work or not: ```typescript @Directive({ selector: 'input[type="number"]', standalone: true, host: { 'class.invalid': 'value', }, }) export class PositiveNumberDirective { @Input() enabled = true; private readonly elRef = inject(ElementRef); get value() { const value = this.elRef.nativeElement.value ?? 0; return this.enabled && (+value) < 0; } } ``` This works, but it has three downsides: 1. We have to provide the boolean with a value each time we *don't want* the directive to work, which is not a big deal, but it is still a bit of a hassle. 2. We might have to use the `enabled` property multiple times in the directive, as it possibly can have several host listeners, host bindings, etc. This makes the directive harder to read and understand. 3. Finally, the directive *still* gets applied, making Angular spend some unnecessary computational power on it. Now, let's see how we can solve this problem with a selector negation, more commonly known as the `:not()` selector. We can use it like this: ```typescript @Directive({ selector: 'input[type="number"]:not([allowNegative])', standalone: true, host: { 'class.invalid': 'value', }, }) export class PositiveNumberDirective { private readonly elRef = inject(ElementRef); get value() { const value = this.elRef.nativeElement.value ?? 0; return (+value) < 0; } } ``` As we can see, the directive's code/logic remained the same, we just changed the selector, excluding all elements that have the `allowNegative` attribute. Now, we can apply this "negative" attribute whenever we want to allow negative numbers, and the directive will not apply to that input. ```html ``` The `:not()` selector specifier can accept any type of selector that is valid for a directive selector. You can find a working reproduction of this example [here](https://stackblitz.com/edit/stackblitz-starters-tcfysm?file=src%2Fmain.ts&ref=angularspace.com). Finally, as we have covered all of the scenarios, it is time we briefly discuss some pitfalls we might encounter when working with directives. ## The Pitfalls With all the power of the selectors we saw in this article, it might be tempting to assume that it works exactly like a CSS selector. However, this could not be further from the truth. Let's see three main scenarios where this falls short. ### Cannot target child-parent relations While it certainly would be useful, it is impossible to target elements that are children of some specific parents only. We could try this: ```typescript @Directive({ selector: 'div > button', standalone: true, host: { '(click)': 'onClick()', }, }) export class ParentChildDirective { onClick() { console.log('clicked') } } ``` And then put this template: ```html
``` When we click on the first button, we might rejoice as it will log "clicked". However, it will also log when we click on the second button, which is not inside a `div` element. This is because Angular just picked the last element from the confusing (from its perspective) selector, and applied the directive to it. So, no complex parent-child (or sibling, ancestor, descendant, etc.) relations for directive selectors. Remember, **Angular directive selectors are not exactly CSS selectors**. ### Directives cannot be applied dynamically While it might certainly seem so, directives won't be "added" and "removed" as soon as an HTML element begins to match a certain selector. This becomes readily obvious with classes as directive selectors. When we built the `InvalidTooltipDirective`, we mentioned that it has a glaring problem; the thing is, it might create the impression that whenever an element obtains the `invalid` class, the directive will automatically apply to it; this is simply not the case: Angular directives get applied at compile-time, meaning this directive will be applied to elements that already have the `invalid` class, but not to those that will obtain it later. You can see an example of how this works (or not works) [here](https://stackblitz.com/edit/stackblitz-starters-2qjx9u?file=src%2Fmain.ts&ref=angularspace.com). ### Structural directives can only be attributes With structural directives, what happens is that Angular uses some clever syntactic sugar: an element marked with an asterisk automatically becomes wrapped in an ``. So, if we just type, say, `
Text
`, it will automatically translate to `
Text
`. This is why structural directives can only be attributes, as they are applied to the `` element, not the element that is inside it. We cannot, for instance, target the `div` element in the previous example with a structural directive, as it is not the element that is marked with the asterisk. While this certainly restricts us from accomplishing some things, it is not very big of a deal, as we rarely need to do any logic in this regard. ## Conclusion I enjoyed writing this article series, and I hope it proved useful to readers as well. Angular directives are super powerful and underused, so I hope that this series will help everyone utilize them more often. My experience proved that doing this greatly simplifies codebases, especially the templates. ## Small promotion ![Modern Angular.jpeg](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/Modern-Angular.jpeg) This article series took a long time to write (almost a year!). During that year, I was also busy writing a book. It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular?ref=angularspace.com). The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on [Twitter](https://twitter.com/Armandotrue?ref=angularspace.com) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/?ref=angularspace.com), where I will be posting whenever there are news or promotions available. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/Screenshot-2024-08-08-at-09.31.49-1.png) ### Content projection using ng-content URL: https://www.angularspace.com/content-projection-using-ng-content/ Last updated: 2024-08-04T23:18:29.000Z In the world of modern web development, building reusable and flexible components is crucial. Angular, provides a powerful feature known as [**ng-content**](https://angular.dev/api/core/ng-content?ref=angularspace.com#mat-tab-content-107-0) to achieve this. This feature allows developers to project content from a parent component into a child component, making it easier to create dynamic and customizable components. In this blog post, we'll explore what [**ng-content**](https://angular.dev/api/core/ng-content?ref=angularspace.com#mat-tab-content-107-0) is, how it works, and how you can use it to enhance your Angular applications. ## What is ng-content? [**ng-content**](https://angular.dev/api/core/ng-content?ref=angularspace.com#mat-tab-content-107-0) is an Angular directive used for content projection, which is a technique that enables you to insert content from a parent component into a child component. This is particularly useful for creating reusable components where the content inside the component can be defined by the parent component. ### How Does ng-content Work? When you use [**ng-content**](https://angular.dev/api/core/ng-content?ref=angularspace.com#mat-tab-content-107-0) in a child component, it acts as a placeholder. During runtime, Angular replaces this placeholder with the content passed from the parent component. This mechanism allows for greater flexibility and reusability of components. ### Basic Usage of ng-content Let’s start with a simple example to understand the basic usage of [**ng-content**](https://angular.dev/api/core/ng-content?ref=angularspace.com#mat-tab-content-107-0). ### Child Component (**alert.component.html**) ```html
``` ### Parent Component (**app.component.html**) ```html

This is an important alert message!

``` In this example, the `` tag in the **alert.component.html** acts as a placeholder. The content within the tags in the **app.component.html** is projected into this placeholder, resulting in the following rendered HTML. ```html

This is an important alert message!

``` ## Multiple content placeholders with fallback [ng-content](https://angular.dev/api/core/ng-content?ref=angularspace.com#mat-tab-content-107-0) Angular also allows for more complex content projection using named slots (select attiribute). Named slots enable you to define multiple placeholders within a single component, each of which can be filled with different content from the parent component. ### Child Component (card.component.html) ```html
``` ### Parent Component (app.component.html) ```html

Header Content

This is the main content of the card.

``` Selector tags will determine where the content gets projected. In this example **card-header** will project header content to **select="card-header"** in the child component. Angular also can fallback to a default ` ` if it cannot find a matching select attribute to project the content on from its parent. Something like this ### Child Component (card.component.html) ```html
``` ### Parent Component (app.component.html) ```html

Header Content

This is the main content of the card.

This will be projected into default ng content in the child

``` While all these were simple use cases of ng-content promoting re-usability, the true potential of ng-content is exposed when it is used with [**ng-template**](https://angular.dev/api/core/ng-template?ref=angularspace.com). In the following scenario there exists a card component which can show data of a student as well as a teacher. Let’s explore how we can re-use the same card component for showing the details of the teacher or the student without using conditionals blocks like [@if](https://angular.dev/api/core/@if?ref=angularspace.com). Let’s first see the student component. ### student-card.component.ts ```javascript import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FakeHttpService, randStudent, } from '../../data-access/fake-http.service'; import { StudentStore } from '../../data-access/student.store'; import { CardComponent } from '../../ui/card/card.component'; import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-student-card', template: ` {{ student.firstName }} `, standalone: true, styles: [ ` .bg-light-green { background-color: rgba(0, 250, 0, 0.1); } `, ], imports: [CardComponent, ListItemComponent, AsyncPipe], changeDetection: ChangeDetectionStrategy.OnPush, }) export class StudentCardComponent { private http = inject(FakeHttpService); private store = inject(StudentStore); students = this.store.students; constructor() { this.http.fetchStudents$.subscribe((s) => this.store.addAll(s)); } addStudent() { this.store.addOne(randStudent()); } deleteStudent(id: number) { this.store.deleteOne(id); } } ``` Now let’s see the teacher component. ### teacher-card.component.ts ```javascript import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FakeHttpService, randTeacher, } from '../../data-access/fake-http.service'; import { TeacherStore } from '../../data-access/teacher.store'; import { CardComponent } from '../../ui/card/card.component'; import { ListItemComponent } from '../../ui/list-item/list-item.component'; @Component({ selector: 'app-teacher-card', template: ` {{ teacher.firstName }} `, styles: [ ` .bg-light-red { background-color: rgba(250, 0, 0, 0.1); } `, ], standalone: true, imports: [ListItemComponent, AsyncPipe, CardComponent], changeDetection: ChangeDetectionStrategy.OnPush, }) export class TeacherCardComponent { private http = inject(FakeHttpService); private store = inject(TeacherStore); teachers = this.store.teachers; constructor() { this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t)); } addTeacher() { this.store.addOne(randTeacher()); } deleteTeacher(id: number) { this.store.deleteOne(id); } } ``` The interesting thing to notice here is the [#rowRef (template reference)](https://angular.dev/api/core/TemplateRef?ref=angularspace.com). We will read the reference in the child component. Here is the child component which is card component. ### app-card.component.ts ```javascript import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, contentChild, input, output, TemplateRef, } from '@angular/core'; @Component({ selector: 'app-card', template: `
@for (item of items(); track item.id) { }
`, standalone: true, imports: [NgTemplateOutlet], host: { class: 'border-2 border-black rounded-md p-4 w-fit flex flex-col gap-3', }, changeDetection: ChangeDetectionStrategy.OnPush, }) export class CardComponent { items = input.required(); add = output(); rowTemplate = contentChild>('rowRef'); // Signal } ``` Notice how the projected content from student card component and teacher card component is accessed via [**contentChild**](https://angular.dev/api/core/contentChild?ref=angularspace.com). In this case we get access to the template ‘rowRef’ passed from parent components. In this way we can show either the content of the teacher or the content of the student without passing [@if](https://angular.dev/api/core/@if?ref=angularspace.com) and introducing any additional components or variables. Here is a link to the final application. Please use this to get an idea of the overall implementation. [Project Link](https://stackblitz.com/edit/stackblitz-starters-dru4ft?file=src%2Fcomponents%2Fapp%2Fapp.component.ts&ref=angularspace.com) Artcile inspired by challenge here. [https://angular-challenges.vercel.app/challenges/angular/1-projection/](https://angular-challenges.vercel.app/challenges/angular/1-projection/?ref=angularspace.com) Some more advanced reading. [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FComponents/Using%5Ftemplates%5Fand%5Fslots?ref=angularspace.com) ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/finalapp.png) Final application with both student and teacher card --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/Screenshot-2024-08-05-at-01.15.15.png) ### NEW 10x e-Book Giveaway! - Mastering Node.js Web Development by Adam Freeman URL: https://www.angularspace.com/new-10x-e-book-giveaway-mastering-node-js-web-development-by-adam-freeman/ Last updated: 2024-07-25T12:29:34.000Z Another Giveaway! You probably know almost everything when it comes to the frontend and Angular. Make sure to also brush up on your full-stack abilities with this fantastic Node E-Book by Adam Freeman x PACKT Publishing! - x10 E-books up for grabs! ### Who this book is for > This book is for programmers with a basic knowledge of HTML and CSS who are transitioning into JavaScript development and are looking to master the implementation of server-side applications. - 778 Pages - ... more details in the link below :) Here: [https://www.amazon.com/Mastering-Node-js-Web-Development-comprehensive/dp/1804615072](https://www.amazon.com/Mastering-Node-js-Web-Development-comprehensive/dp/1804615072?ref=angularspace.com) Retail Price: $39,99 Instructions on how to participate in giveaway below 👇 _This post is for subscribers only._ ### 10x e-Book Giveaway! - Reactive Patterns with RxJS and Angular Signals by Lamis Chebbi URL: https://www.angularspace.com/10x-e-book-giveaway-reactive-patterns-with-rxjs-and-angular-signals-by-lamis-chebbi/ Last updated: 2024-07-19T10:48:56.000Z It was a long time since we had a giveaway :). Today [**GDE Lamis Chebbi**](https://x.com/LamisChebbi?ref=angularspace.com) has something special for you. You can win **one of 10 copies** of her **Reactive Patterns with RxJS and Angular Signals** e-book! We have organized this cooperating with **PACKT Publishing.** RXJS + SIGNALS = PERFECTION > This second edition aligns with the latest version of Angular, introducing new reactive patterns based on Angular Signals, which play a pivotal role in enabling fine-grained reactivity within Angular and enhancing change detection and user interface rendering. Learn more about e-book here: [https://www.amazon.com/Reactive-Patterns-RxJS-Angular-Signals-ebook/dp/B0CTXL7HZP/ref=tmm\_kin\_swatch\_0?\_encoding=UTF8&qid=&sr=](https://www.amazon.com/Reactive-Patterns-RxJS-Angular-Signals-ebook/dp/B0CTXL7HZP/ref=tmm%5Fkin%5Fswatch%5F0?%5Fencoding=UTF8&qid=&sr=&ref=angularspace.com) Instructions on how to participate in giveaway below 👇 _This post is for subscribers only._ ### VM$ pattern in Angular URL: https://www.angularspace.com/vm-pattern-in-angular/ Last updated: 2024-07-17T05:30:20.000Z Hello Angular Space Community! As an Angular developer, you may face issues when your component has to use multiple asynchronous sources. Beginner Angular developers may encounter some minor problems with that and might not solve them correctly, so I want to show you a simple trick. ### Case Study - Handle multiple Observables inside component Angular emphasize to write reactive code with Observable/Promise - HTTP Requests, `valueChanges` in FormControl, `navigate` method in Router - even third-party libraries using this methodology, like `@ngrx/store`, where you can access to state using selectors which are Observables. Let's prepare example component where we want to display information from multiple sources. ```ts @Component({ selector: 'app-pseudo-page', standalone: true, imports: [AsyncPipe], templateUrl: './pseudo-page.component.html', styleUrl: './pseudo-page.component.css', }) export class PseudoPageComponent { private readonly todoService = inject(TodoListService); private currentPageSubject = new BehaviorSubject(1); readonly currentPage$ = this.currentPageSubject.asObservable(); private readonly currentUser$ = inject(CurrentUserService).currentUser$; readonly loggedTime$ = interval(1000); readonly todoList$ = this.currentPage$.pipe( switchMap((page) => this.todoService.getTodos(page, 10)) ); prevPage() { this.currentPageSubject.next( this.currentPageSubject.value - 1 < 1 ? 1 : this.currentPageSubject.value - 1 ); } nextPage() { this.currentPageSubject.next( this.currentPageSubject.value + 1 > 10 ? 10 : this.currentPageSubject.value + 1 ); } ``` To display them on the template we use `Async` pipe - this one makes subscription by their own, will update the template when data will be changed and unsubscribe it when the component will be destroyed. We don't want to manage subscription manually. Let's apply data in template. For this one, we use trick `async + as XYZ` inside `@if`. ```html

Hello {{ currentUser$ | async }} !

You are logged for {{ (loggedTime$ | async) || 0 }} s

There is your todo-list

    @for (element of (todoList$ | async); track element.id) {
  • {{ element.title }}
  • }
{{currentPage$ | async}}
``` At first sight, the component code looks acceptable, but the template starts to look a little bit messy. We need to notice, even if `async` is our friend in this case, each of them is a different subscription, which may cause huge issues with performance and code management. To improve that, we can implement *ViewModel* pattern. ### View-Model in Angular *View Model* is a part of MVVM pattern (Model-View-ViewModel) and it's about seperate Model (M) from View (V) and create some kind of bridge to connect them - View Model (VM). In other words - ViewModel is an object, which (with Model's helps) decide which data will be passed into view. Using Angular parts we can explain this Architecture like: - Model is Component and all data what we have there - View is Components's template - View-Model is component property, which we will use inside template ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/mvvm.png) ### Practical example With this knowledge, let's try to refactor our code. First - let's prepare variable, which we pass into our template. To keep the naming standard - let's call it `vm$`. We use here functions [combinateLatest](https://rxjs.dev/api/index/function/combineLatest?ref=angularspace.com#combinelatest) operator and [map](https://rxjs.dev/api/index/function/map?ref=angularspace.com). The first one will be used as combination of our all sources, the second to change value of each stream into one object. ```ts readonly vm$ = combineLatest([ this.loggedTime$, this.currentPage$, this.currentUserService.currentUser$, this.todoList$, ]).pipe( map(([loggedTime, currentPage, currentUser, todoList]) => { return { loggedTime, currentPage, currentUser, todoList } }) ``` And that's all we need to do inside our component. And now we need to pass this variable to our template just like we already did, but instead of multiple `async` pipe, we have only one, with all data which we're interested in. Because `vm$` does refer to whole component's data - it's good to put at the top of the component template. ```html @if (vm$ | async; as vm) {

Hello {{ vm.currentUser }} !

You are logged for {{ vm.loggedTime }} s

There is your todo-list

    @for (element of vm.todoList; track element.id) {
  • {{ element.title }}
  • }
{{vm.currentPage }}
} ``` And that's it, from now on, the component will be updated every time when one of sources from `combinateLatest` emit a new value. Even if you set `OnPush` change detection strategy in this component, you will be almost sure that your component will be rerendered without calling `detectChanges()` from `ChangeDetectorRef`. `Async` pipe is one of few solutions prepared from Angular team where when in changes your component is flagged for update. This pattern learn you and your team how to separate things for component and for template. As you can see - the template know, which function from component it may call, it has access to some data - "some" data, not all of them. Additionally we can manipulate this streams values with other operators, like `switchMap`, `delay`, `map` etc. ```ts readonly vm$ = combineLatest([ // our source is "interval" but we want to display in template nice formated time m:ss this.loggedTime$.pipe( map((time) => { const minutes = Math.floor(time / 60); const seconds = time - minutes * 60; return [minutes, ('0' + seconds).slice(-2)]; }), map((time) => time.join(':')), startWith(0) ), this.currentPage$, this.currentUserService.currentUser$, this.todoList$.pipe(startWith([])), ]).pipe( map(([loggedTime, currentPage, currentUser, todoList]) => { return { loggedTime, currentPage, currentUser, todoList, }; }) ); ``` ### Hold on, I think I already saw it... Yup, this pattern is recommended when you use `@ngrx/store` and `@ngrx/component-store`. Store is asynchronous, selectors returns Observable with data, so in case when you have to use multiple selectors from store in one component - using `vm$` is recommended. ### No Control-Flow in template? No problem! You may ask - "How can I use it in my application? I still use the oldest version of Angular and I don't have control-flow in my templates..." and I'm happy to tell you that it is the pattern - It's not require any special libraries or specific Angular version , we can change tools to achieve our goals, so you can still use it! As you already know, before control-flow (`@if`, `@for` etc) we use same "structural directives" like `*ngIf`, `*ngFor` etc. You can use them for creating subscription in your template (just like we already do with `@if`) on `` or other element. ```html

Hello {{ vm.currentUser }} !

You are logged for {{ vm.loggedTime }} s

...
``` I feel obligated to mention that there are some prepared solutions dedicated mainly for managing asynchronous values/state in template like for ex. [RxLet directive from @rx-angular/template package](https://www.rx-angular.io/docs/template/api/rx-let-directive?ref=angularspace.com), which solve some issues and do improvements compare witth @if/\*ngIf. ## What's next? `@let` instead of `@if` Noteworthy is fact that Angular 18.1 will introduce the [@let syntax](https://github.com/angular/angular/pull/55848?ref=angularspace.com). This will allow you declare variables directly inside component template. Using new syntax, you can remove `@if` block and create directly vm property ```html @let vm = (vm$ | async);

Hello {{ vm.currentUser }} !

You are logged for {{ vm.loggedTime }} s

``` ### Conclusion The solution I just described above (`@if` \+ `async` \+ `combinateLatest`) is probably the most popular and universal, but sometimes additional modification will be required. - `combinateLatest` emit value when every sources emits their own. If one of them didn't emit value, `combinateLatest` will wait for them. In order to fix it, you can try to use `startWith` operator on source. - Using `@if` to assign property from `async` may cause some unexpected issues. If whole `vm` will be "falsy value" (0, false, null etc) this directive will not render the component. In this article I wanted to show you this technique. I use them every day and - for me - this is the base of my work to create "smart components". Example code which I use in this article is available in [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-zwxfv3?file=src%2Fpseudo-page%2Fpseudo-page.component.ts&ref=angularspace.com), feel free to play with it! --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/Screenshot-2024-07-17-at-02.17.33.png) ### Angular SSR - Platform Provider Pattern URL: https://www.angularspace.com/angular-ssr-platform-provider-pattern/ Last updated: 2024-07-09T12:03:01.000Z As Angular developers flock to the new features of SSR, they'll inevitably run into issues with browser specific APIs trying to run on the server. Just try to render any web components. But not all is lost if you're strictly using Angular components. However, what about certain services that that require browser specific APIs? ## **Problem** Imagine there's a `ClaimsDirective` that reads a JWT token. The token contains role based claims. On the browser, the JWT is read and stored into sessionStorage. When the server tries to read the JWT from sessionStorage the server throws an error. ### **PLEASE NOTE** This is merely an example using JWT tokens as a way to read claims from different storage. I'm assuming the token has been validated server side before rendering any sort of sensitive information. ## Goals The main goal is to exhibit no differences between the server side rendered markup and the client. 1\. Define a contract for the Client and Server 2\. Understand which Context the Service is Running 3\. Implement the JwtToken Services 4\. Configure the Providers 5\. Create ClaimsDirective ### Before You Begin A working example exists in the [ngserveio-platform-provider-pattern repository](https://github.com/mutableideas/ngserveio-platform-provider-pattern?ref=angularspace.com) ## **Define the Service Contract** The services will implement the same interface. Given the goal is to create a claims service for JWT tokens, each service includes a `getClaims(): Nullable` and a `setClaims(token: string): void`. ```javascript import { Nullable } from '@ngserveio/utilities'; export interface IJwtClaimsService { getClaims(): Nullable; setToken(token: string): void; } ``` Depending on the needs of the application using the JWT token, the storage of the token could differ between platforms. So getClaims accepts a generic type T. With the interface defined, how will the application determine which instance to create to return the claims? ## **Understand which Context the Service is Running** Angular ships a couple of helper functions and an injection token out of @angular/common to help determine the platform context it's running. The `PLATFORM_ID` injection token's value can be supplied to the `isPlatformBrowser` or the `isPlatformServer` function to determine where the code is executing. How do we choose the correct service given the platformId and inject the correct service? Components and services dependent on platform specific services shouldn't be responsible for determining platform context and choosing the correct service to use. It'd muddy the code quickly with platform checks. The platform services they do consume should also adhere to an interface. In this case, the services will adhere to `IJwtClaimsService` interface. This is where the power of generics and the factory pattern shine. The `platformProviderFactory` accepts a generic T. Two `Type` parameters are supplied for the browser and server. The `PLATFORM_ID` and `isPlatformServer` determine the platform context and choose which service to instantiate. ```javascript import { isPlatformServer } from "@angular/common"; import { PLATFORM_ID, Type, inject } from "@angular/core"; /** * Chooses the service on the isPlatformBrowser check * @param browser - The browser based service * @param server - Server based service * @returns */ export const platformProviderFactory = (browser: Type, server: Type): T => { const platformId = inject(PLATFORM_ID); const platformType: Type = isPlatformServer(platformId) ? server : browser; return inject(platformType); } ``` How do these services differ and for JWT tokens returning claims? ## **Implement the JWT Token Services** Reading the token is dependent where it's stored. The JwtToken services differ on which platform specific storage service to use. Parsing the claims from the token should be consistent for both services. The `parseClaims` function ensures the token is split correctly and returns the payload of the JWT. It can be used by both the server and browser services we define later. ```javascript export const parseClaims = ( token: Nullable ): Nullable => { const claims = token?.split('.'); if (claims?.length !== 3) { return null; } const parsedToken = atob(claims[1]); return JSON.parse(parsedToken) as T; }; ``` The browser JwtToken service stores the JWT into session storage and implements the `IJwtClaimsService` interface. The claims are read and parsed from the session storage key `JWT_TOKEN`. ```javascript import { Injectable } from '@angular/core'; import { Nullable } from '@ngserveio/utilities'; import { IJwtClaimsService } from './consts'; import { parseClaims } from './token-claims.helper'; @Injectable({ providedIn: 'root', }) export class SessionStorageJwtService implements IJwtClaimsService { protected readonly JWT_TOKEN = 'JWT_TOKEN'; protected get storage(): Storage { return sessionStorage; } getClaims(): Nullable { const token = this.storage.getItem(this.JWT_TOKEN); return parseClaims(token); } setToken(token: string): void { this.storage.setItem(this.JWT_TOKEN, token); } } ``` The server JwtToken is read from the headers in a cookie. This implementation requires the request to be passed to the service. Injection tokens `REQUEST` / `RESPONSE` pass an express request / response to the `CookieJwtService`. ```javascript import { InjectionToken } from "@angular/core"; import { Request, Response } from 'express'; export const REQUEST = new InjectionToken('REQUEST'); export const RESPONSE = new InjectionToken('RESPONSE'); ``` The `CookieJwtService` implements the `IJwtClaimsService`. The claims are read from the `JWT_TOKEN` cookie on the request and parsed into the generic `T` claims object. ```javascript import { Injectable, inject } from '@angular/core'; import { Nullable } from '@ngserveio/utilities'; import { parseClaims } from './token-claims.helper'; import { IJwtClaimsService } from './jwt-service.interface'; import { REQUEST, RESPONSE } from './request.token'; import { JWT_COOKIE_NAME } from './consts'; @Injectable({ providedIn: 'root', }) export class CookieJwtService implements IJwtClaimsService { protected readonly request = inject(REQUEST, { optional: true, }); protected readonly response = inject(RESPONSE, { optional: true }); getClaims(): Nullable { const token = this.request?.cookies[JWT_COOKIE_NAME]; return parseClaims(token); } setToken(token: string): void { this.response?.cookie(JWT_COOKIE_NAME, token); } } ``` ## **Configure the Providers** Using the `platformFactoryProvider` function created earlier, the `jwtClaimsService` function defines the browser and server claims services. This is the only function exported out of this library. Developers won't need to worry about which service to choose. The `jwtClaimsService` can be invoked in any injection context. ```javascript import { SessionStorageJwtService } from './session-storage-jwt.service'; import { CookieJwtService } from './cookie-jwt.service'; import { platformProviderFactory } from './platform-provider.factory'; import { IJwtClaimsService } from './jwt-service.interface'; export const jwtClaimsService = () => platformProviderFactory( LocalStorageJwtService, CookieJwtService ); ``` The server context needs to pass along the express request object. Using the `REQUEST` / `RESPONSE` injection tokens, the `CommonEngine` requires these providers be injected into `CookieJwtClaimsService`. ```javascript // All regular routes use the Angular engine server.get('*', (req, res, next) => { const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap, documentFilePath: indexHtml, url: `${protocol}://${headers.host}${originalUrl}`, publicPath: browserDistFolder, providers: [ { provide: APP_BASE_HREF, useValue: baseUrl }, { provide: REQUEST, useValue: req }, // ADD THIS { provide: RESPONSE: useValue: res } // ADD THIS ], }) .then((html) => res.send(html)) .catch((err) => next(err)); }); ``` The `jwtClaimsService` can be injected into any component or service requiring claims from the JWT. ## **Create the ClaimsDirective** It could be argued the app doesn’t need to be server side rendered behind an authorized route. But why make the browser do the work the server can do from the beginning? Even authorized routes that are server side rendered make a better more performant experience. The `ClaimsDirective` accepts a role checking if the role exists in the claims. The `jwtClaimsService` function provides us the correct service to use to retrieve the claims. Using the `getClaims` method, the claims are returned and the role is compared against the directive's input. ```javascript import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core'; import { jwtClaimsService } from './jwt.service'; import { RolesEnum } from './role.type'; import { Claims } from './claims.type'; @Directive({ selector: '[orgClaims]', standalone: true, }) export class ClaimsDirective { protected jwtClaimsService = jwtClaimsService(); protected templateRef = inject(TemplateRef); protected viewContainer = inject(ViewContainerRef); @Input({ required: true }) public set orgClaims(value: RolesEnum) { const userRole = this.jwtClaimsService.getClaims(); // This is only an example permissions would have a hierarchy of access // and your logic would reflect that better here if (userRole?.role === value) { this.viewContainer.createEmbeddedView(this.templateRef); return; } this.viewContainer.clear(); } } ``` Determining which block of code to show the user, the `ClaimsDirective` is applied showing which role the user belongs. ```HTML

Name: {{ claims?.name || 'No Name provided' }}

Email: {{ claims?.email || 'No Email Provided' }}

I Am an Admin

I Am an Editor

I Am an Viewer

``` If working in the [ngserveio-platform-provider-pattern repository](https://github.com/mutableideas/ngserveio-platform-provider-pattern?ref=angularspace.com), start the sample application by running the following commands. ```bash ng build node ./dist/ngserveio-platform-provider-pattern/server/server.mjs ``` A login page shows initially. Click the login button as this will generate a sample token with claims and add the token to session storage. The page shows the claims in the page including the I am an Admin text per the role. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/angular-ssr-providers-claims-page.png) Page showing the name and I Am an Admin ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/angular-ssr-providers-source-claims-page.png) Showing the rendered HTML from SSR ## **Conclusion** The global api surface will differ base on the platform the code runs. Server side render relies on the request headers for handling requests. In this case, a HttpOnly cookie is unavailable for the browser to read. The `sessionStorage` is unavailable for Node to read. Our code needs to understand where to read the JWT token. Determining storage mechanism to be used in the platform can done through using the `isPlatformBrowser` function. Without littering the code with this function, the `platformServiceProvider` registers and chooses which service to use with a common interface. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/Screenshot-2024-07-09-at-13.24.50.png) ### Synchronized Storage with Signals URL: https://www.angularspace.com/synchronized-storage-with-signals/ Last updated: 2024-07-04T08:22:50.000Z Working with local storage in JavaScript is both very easy and sometimes frustrating. While being really straightforward, the [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FStorage%5FAPI?ref=angularspace.com) lack of one critical feature that is more deeply rooted than ever in modern Angular applications: reactivity. Fortunately, with the latest release, many tools are at our disposal to create a handy utility function, creating a reactive signal from a value stored locally! In this article, we will see how to abstract the web storage in a dedicated Angular service, and how to take advantage of it to synchronize its value using signals to achieve the following outcome: ![Final Result GIF](https://raw.githubusercontent.com/pBouillon/DEV.StorageSignal/main/projects/demo/public/final-result.gif) > All the code of this article is [hosted on GitHub](https://github.com/pBouillon/DEV.StorageSignal?ref=angularspace.com) if you would like to check it out! ## Abstracting the Web Storage Before actually building the signal's logic, we will first have to abstract the native Web Storage API. This will allow us to chose what we are manipulating, how, as well as giving us more flexibility on what kind of storage we would use (not to mention an easier way of mocking it for testing purposes). ### Dynamic Storage Type The first thing we would like to do is to define an [injection token](https://angular.dev/guide/di/lightweight-injection-tokens?ref=angularspace.com#using-lightweight-injection-tokens) for the kind of storage to use: ```ts // 📂 storage.service.ts export const STORAGE = new InjectionToken( 'Web Storage Injection Token' ); ``` From now we can provide the desired `Storage` (`localStorage`, `sessionStorage`, etc.) to our Angular application: ```ts // 📂 main.ts bootstrapApplication(AppComponent, { providers: [{ provide: STORAGE, useValue: localStorage }], }).catch((err) => console.error(err)); ``` ### Creating the `StorageService` Since the `Storage` is now known in the injection container, we can consume it in an Angular service to manage the read and write operation, while also enforcing type safety (within a certain limit): ```ts // 📂 storage.service.ts @Injectable({ providedIn: 'root' }) export class StorageService { readonly #storage = inject(STORAGE); getItem(key: string): T | null { const raw = this.#storage.getItem(key); return raw === null ? null : JSON.parse(raw) as T; } setItem(key: string, value: T | null): void { const stringified = JSON.stringify(value); this.#storage.setItem(key, stringified); } } ``` Now that the basic building blocks are ready, we can work on the actual signals! ## Leveraging Signals Signals are really easy to use but also really [easy to wrap and extend](https://dev.to/this-is-angular/efficient-angular-effects-patterns-4396?ref=angularspace.com). Before diving into read and write synchronization, let's first create the utility function: ```ts // 📂 from-storage.function.ts export const fromStorage = (storageKey: string): WritableSignal => { const storage = inject(StorageService); const initialValue = storage.getItem(storageKey); return signal(initialValue); } ``` For now, this only create a `signal` from a value (or its absence) in the injected `Storage`. Upon calling `fromStorage`, we can now track a specific key and its (typed) value: ```ts // 📂 app.component.ts type ColorScheme = 'light' | 'dark'; @Component({ /*...*/ }) export class AppComponent { readonly preferredTheme = fromStorage('preferred-theme'); } ``` This looks promising, but we still lack of that desired reactivity in two major cases: - When updating the value, we should also update the stored value - When any update is made to the storage for this key, we should also update the value Let's address those! ## Syncing Writes Updating the value in the `Storage` whenever we are updating the `signal`'s value is the easiest part. With `effect` we can simply call `StorageService.setItem` to write the updated value since it will be invoked when the value actually changes (or [matches the defined equality](https://angular.dev/guide/signals?ref=angularspace.com#signal-equality-functions)): ```ts // 📂 from-storage.function.ts export const fromStorage = (storageKey: string): WritableSignal => { // ... const fromStorageSignal = signal(initialValue); const writeToStorageOnUpdateEffect = effect(() => { const updated = fromStorageSignal(); untracked(() => storage.setItem(storageKey, updated)); }); return fromStorageSignal; } ``` That's one problem solved! ## Syncinc Reads: Taking Advantage of the Web Storage API The main issue here is that the update can occur in two cases that we can't always control: - Another piece of code updates the stored value - Another tab updates the same value We will need a way of detecting that something changed, possibly outside of our app. ### Using `setTimeout` A first solution we might think of would be using `setTimeout`. While possible, it would either introduce a latency in the update with a long period, or a heavy polling mechanism if we chose a smaller one (not to mention that watching several values would then introduce a lot of those polling systems). Such a system could be implemented as: ```ts // 📂 from-storage.function.ts export const fromStorage = (storageKey: string): WritableSignal => { // ... const updateSignalOnSignalWriteEffect = effect((onCleanup) => { const intervalId = setInterval(() => { const newValue = storage.getItem(key); const currentValue = fromStorageSignal(); const hasValueChanged = newValue !== currentValue; if (hasValueChanged) fromStorageSignal.set(newValue); }, 150) onCleanup(() => clearInterval(intervalId)); }); return fromStorageSignal; } ``` > 🚨 In applications that are still using zonejs for change detection, you should run this outside zone to avoid triggering unecessary change detection: ```ts inject(NgZone).runOutsideAngular(() => /*...*/ ); ``` ### Using the storage event What if instead of a polling mechanism we could *react* to a change? Fortunately, the Web Storage API defines a [Storage Event](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage%5Fevent?ref=angularspace.com) that can be listened to using `storage.onstorage` or the `storage` event, which tells us that something in the `Storage` has been modified, what key was targeted and some more information. Sounds great, let's use that instead: ```diff // 📂 from-storage.function.ts export const fromStorage = (storageKey: string): WritableSignal => { ... - const updateSignalOnSignalWriteEffect = effect((onCleanup) => { - ... - }); + const storageEventListener = (event: StorageEvent) => { + const isWatchedValueTargeted = event.key === storageKey; + if (!isWatchedValueTargeted) { + return; + } + + const currentValue = fromStorageSignal(); + const newValue = storage.getItem(storageKey); + + const hasValueChanged = newValue !== currentValue; + if (hasValueChanged) { + fromStorageSignal.set(newValue); + }; + } + + window.addEventListener('storage', storageEventListener); + + // 👇 Don't forget to clean up after yourself + inject(DestroyRef).onDestroy(() => { + window.removeEventListener('storage', storageEventListener); + }); return fromStorageSignal; } ``` Great! Let's try it: ```ts // 📂 app.component.ts @Component({ /*...*/ }) export class AppComponent { readonly preferredTheme1 = fromStorage('preferred-theme'); togglePreferredTheme(): void { this.preferredTheme1.update(current => current === 'light' ? 'dark' : 'light'); } readonly preferredTheme2 = fromStorage('preferred-theme'); setLightTheme(): void { this.preferredTheme2.set('light'); } } ``` Calling `setLightTheme` does not update `preferredTheme1` and `togglePreferredTheme` has no effect of `preferredTheme2`! I thought we just solved this problem? The explanation is actually on [the MDN doc page](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage%5Fevent?ref=angularspace.com) where it explains the reason why it won't work on the same browsing context. In other words, it means that updating the value ourselves won't trigger the event in our own tab. We were so close to a reactive value! Fortunately, the `StorageEvent` can be crafted and we have our own service to interact with the `storage`, meaning that we can raise that event ourselves upon write: ```ts // 📂 storage.service.ts @Injectable({ providedIn: 'root' }) export class StorageService { readonly #storage = inject(STORAGE); getItem(key: string): T | null { /*...*/ } setItem(key: string, value: T | null): void { const stringified = JSON.stringify(value); this.#storage.setItem(key, stringified); // 👇 Notify of the update const storageEvent = new StorageEvent('storage', { key: key, newValue: stringified, storageArea: this.#storage, }); window.dispatchEvent(storageEvent); } } ``` > 🚨 Be aware that this can duplicate events for the other tabs, hence the need of checking if the value has changed in the event handler to avoid any issue. If we try again to call `togglePreferredTheme` or `setLightTheme`, we can see that both signals are now indeed updated along with the value in the `Storage`. We did it! ## Wrapping up In this article we abstracted both the `Storage` and the interaction with the Web Storage API in order to have control of its usage. We then introduced a method to create a signal from a key to watch a value in the `Storage`, achieving synchronization between signals and the Web Storage: ![Final Result GIF](https://raw.githubusercontent.com/pBouillon/DEV.StorageSignal/main/projects/demo/public/final-result.gif) If you would like to play with the code yourself, check out [the code on GitHub](https://github.com/pBouillon/DEV.StorageSignal?ref=angularspace.com)! --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/Screenshot-2024-06-20-at-22.01.46.jpg) ### Angular inputs & lifecycle URL: https://www.angularspace.com/angular-inputs-lifecycle/ Last updated: 2024-07-01T15:36:54.000Z Inputs don’t always work as expected in Angular. Even now, quite a few years after the Angular 2 breakdown, I hear and see in action a misconception about Angular inputs and lifecycle. How does it read ? Something like that : > The OnInit Angular lifecycle method is running when inputs have a value, so you should not read inputs values in the constructor, but instead, in the`ngOnInit `method. But honestly, this statement might not reflect properly how things actually work. When I do code reviews, I see that this assumption leads to additional problems. People assume that the code that handle changes can only be declared in the `ngOnInit` method. An untrained developer will assume this scenario quite often. However, If we are talking about a simple component where the value is hardcoded, it will be ok to have it declared there. ```typescript export class ParentComponent { pouf = 'pouf'; } export class ChildComponent implements OnInit { @Input({ required: true }) pouf!: string; ngOnInit(): void { // here you have your input value console.log(this.pouf); } } ``` ## Handle inputs when you need them [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/aboudard/inputs-lifecycle/inputs-lifecycle.md?ref=angularspace.com#handle-inputs-when-you-need-them) It has been written quite a lot of articles about how to manage inputs, the different methods, using a `setter` and a `getter` methods … and so on. [Detecting @​Input changes in Angular with ngOnChanges and Setters - by Todd Motto](https://ultimatecourses.com/blog/detect-input-property-changes-ngonchanges-setters?ref=angularspace.com) I will just give my two cents here, as **why** we have to do that. We take the previous example and instead of a simple value, we produce an Observable and give it to the child component. ```typescript export class ParentComponent { title$ = of('Alain').pipe(delay(2000)); } export class ChildComponent implements OnInit { @Input({ required: true }) title!: string; ngOnInit(): void { //Here you have null in your input console.log(this.title); } } ``` - We made a delayed Observable to simulate some kind of API call - We see that there is no value when calling `this.title` in the `ngOnInit` method So no, you simply can’t rely on this method to work with your inputs. So yes, every time we can, we should work on a reactive way with our data, to avoid that kind of trouble. Adding the `OnChanges` interface will allow us to monitor this data and work only when it’s really available : ```typescript ngOnChanges(changes: SimpleChanges) { if (changes["title"]) { //I know I have the data console.log(this.title); } } ``` ### Working asynchronously with Inputs [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/aboudard/inputs-lifecycle/inputs-lifecycle.md?ref=angularspace.com#working-asynchronously-wit-inputs) You will need to work with the asynchronous data in different ways. One of the best solution I’ve been trying, was to make these Inputs become Observables. In this article, you create an annotation for that purpose : [How to build reactive Angular Components using Inputs as Observables](https://engineering.leanix.net/blog/reactive-angular-components-inputs-observables/?ref=angularspace.com) The result in your code will look like this (taken from the above article) : ```typescript @Component({ selector: "my-component", template: `

result: {{ result$ | async }}

`, }) export class MyComponent { @Input() prop!: number; @Observe("prop") private prop$!: Observable; result$ = this.prop$.pipe( switchMap((prop) => this.myService.getResult(prop)), // share the response across all subscribers to prevent // multiple HTTP requests share() ); } ``` This code is hiding the complexity of transforming the input into an Observable. You would not have to wonder if the data is available when you need it. You simply write reactive code. ## Working with Angular Signal Inputs [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/aboudard/inputs-lifecycle/inputs-lifecycle.md?ref=angularspace.com#working-with-angular-signal-inputs) But the clever reader will say that of course we don’t use the old annotation API and instead we should use the [Signal Inputs](https://angular.dev/guide/signals/inputs?ref=angularspace.com). Let’s do that, with having still an Observable as the source of data. ```typescript export class ParentComponent { meuh$ = of('meuh').pipe(delay(3000)); } export class ChildComponent implements OnInit, OnChanges { meuh = input.required(); constructor() { effect(() => { //First run: this input is null console.log('Effect : ', this.meuh()); }); } ngOnInit(): void { //Here you have null in your input console.log(this.meuh()); } } ``` - The nature of the data doesn’t change the fact that its source is an Observable. We simply can’t assume the value in the child component, wether it’s a regular Input or a Signal Input. - We still can use the `OnChanges` method to monitor and use this value : ``` ngOnChanges(changes: SimpleChanges) { if (changes["meuh"]) { //I know I have the data console.log(this.meuh()); } } ``` Of course we probably would not use an Angular Signal Input this way, but instead, we would use some reactive code, based on `effect` and `computed`. ### Derive Inputs data [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/aboudard/inputs-lifecycle/inputs-lifecycle.md?ref=angularspace.com#derive-inputs-data) Since we want to use the data of our Inputs and transform it, what options do we have ? Considering we don’t want to use the `ngOnInit` method, we could use the setters, with some issues that [you can read about here](https://kelly-kh-woo.medium.com/angular-stop-using-setter-for-input-or-5bdb4b990ab3?ref=angularspace.com). Namely, you can work safely on one Input with a setter, but as soon as there are more than one, you start having troubles knowing when all inputs are available. Working with the `ngOnChanges` method like mentioned above is a good solution, but it’s not the most elegant one. You could use the `changes` object, but you only have one Input at a time. Yet, you could use it like this : ```typescript export class UserComponent implements OnChanges { @Input({ required: true }) name!: string; @Input({ required: true }) id!: number; fullName?: string; ngOnChanges(changes: SimpleChanges): void { // I could use changes['id'] but then I would need to be sure // that the other Input has value if (this.id && this.name) { this.fullName = this.id + ' - ' + this.name } } } ``` We can see that this is pretty "imperative" code, meaning we make an action when something happens. Using the signals API, we could have a more simple way to use these non synchronous Inputs. This way would be much more "reactive" (and less verbose too) : ```typescript export class UserSignalComponent { id = input.required(); name = input.required(); fullName = computed(() => (this.id() && this.name()) ? this.id() + ' - ' + this.name() : ''); } ``` ## Conclusion [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/aboudard/inputs-lifecycle/inputs-lifecycle.md?ref=angularspace.com#conclusion) If there is a takeway in this article it would be : > Do not rely on Angular **OnInit** lifecycle method to handle your inputs data in your components. Here is some Stackblitz : [https://stackblitz.com/edit/stackblitz-starters-38fyzb](https://stackblitz.com/edit/stackblitz-starters-38fyzb?ref=angularspace.com) Check this great article from [Angular University](https://angular-university.io/?ref=angularspace.com) about the Signal Components in Angular : [Angular Signals Component API: input, output, model (Complete Guide)](https://blog.angular-university.io/angular-signal-components/?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/07/Screenshot-2024-07-01-at-12.17.32.png) ### Unified Control State Change Events in Angular 18 URL: https://www.angularspace.com/unified-control-state-change-events-in-angular-18/ Last updated: 2024-06-26T04:00:23.000Z Angular version 18 has introduced a new event emitter called `events`. It offers more control over the data flow, by allowing for tracking precisely which control is a source of changes, and form state giving access to the form submit and reset events. The `event` field is implemented inside the [AbstractControl](https://angular.dev/api/forms/AbstractControl?tab=description&ref=angularspace.com) class and is available to all classes that inherit from it: [FormControl](https://v17.angular.io/api/forms/FormControl?ref=angularspace.com#description), [FormGroup](https://angular.dev/api/forms/FormGroup?tab=description&ref=angularspace.com), [FormRecord](https://angular.dev/api/forms/FormRecord?tab=description&ref=angularspace.com), and [FormArray](https://angular.dev/api/forms/FormArray?tab=description&ref=angularspace.com). ### Value and state events of a single form control Let’s check how the `events` field works in practice. The code below presents an example component with one [FormControl](https://v17.angular.io/api/forms/FormControl?ref=angularspace.com#description) object named `nameCtrl`. ```typescript import { Component, OnInit } from "@angular/core"; import { FormControl, ReactiveFormsModule } from "@angular/forms"; @Component({ selector: "app-form-control-events", standalone: true, imports: [ReactiveFormsModule], template: ` `, }) export class FormControlEventsComponent implements OnInit { nameCtrl = new FormControl(""); ngOnInit(): void { this.nameCtrl.events.subscribe((event) => { console.log(event); }); } } ``` The control is assigned to the `name` [](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input?ref=angularspace.com) element. There is also a subscription to the `events` field, to log events emitted by the control. Now, let’s type some value into it. Let it be a single character like *'x'*. What we see in the console is that the event object has emitted three events: ![Emitted events logged into the browser console](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/image-1-1.png) Three? Wait… We’ll see the fourth one when we blur the [](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input?ref=angularspace.com) element. ![Emitted events logged into the browser console](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/image-2-1.png) Let's describe each event and describe what it does. Event field emitted four types of events: [ValueChangeEvent](https://angular.dev/api/forms/ValueChangeEvent?ref=angularspace.com), [StatusChangeEvent](https://angular.dev/api/forms/StatusChangeEvent?ref=angularspace.com), [PristineChangeEvent](https://angular.dev/api/forms/PristineChangeEvent?ref=angularspace.com), and [TouchedChangeEvent](https://angular.dev/api/forms/TouchedChangeEvent?ref=angularspace.com). Each of them extends the [ControlEvent](https://angular.dev/api/forms/ControlEvent?ref=angularspace.com) abstract class. All of them contain two fields. First is the `source` field. It points to the object that emitted the event. In our case, events’s source property points to `nameCtrl`. The second field depends on the event type, and contains a value of the control, or value of the control’s state. Now, regarding the example above: **ValueChangeEvent** \- similar to (old fashion ;)) [valueChange](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#valueChanges) event. It provides the current value of the control. By default, it is triggered whenever the value of the control has been changed. **StatusChangeEvent** \- similar to [statusChanges](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#statusChanges). It provides the current validation status. Like [ValueChangeEvent](https://angular.dev/api/forms/ValueChangeEvent?ref=angularspace.com), [StatusChangeEvent](https://angular.dev/api/forms/StatusChangeEvent?ref=angularspace.com) is triggered every time the value of the control has been changed, no matter if the control has assigned validators or not. If the control has an async validator assigned, then [StatusChangeEvent](https://angular.dev/api/forms/StatusChangeEvent?ref=angularspace.com) will be emitted twice. Once, when the status property is set to [PENDING](https://angular.dev/api/forms/FormControlStatus?tab=description&ref=angularspace.com), then for the second time, when the validator returns the result of the validation. **PristineChangeEvent** \- is triggered when the control value is changed for the first time, regardless of whether the control had an initial value. **TouchedChangeEvent** \- is triggered when the user interacts with the control for the first time. Usually, it’s triggered when the user blurs the control, even if the value doesn't change. Since we now know which user actions trigger the event, let’s take a look at how it works with the Reactive Forms API. As we all know, we can change any control's value and status by utilizing methods like [setValue](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#setValue), [disable](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#disable), etc. Using them, we can change the default behavior with options properties like `onlySelf`, `emitEvent`, and so on. Now we can also determine if [ControlEvents](https://angular.dev/api/forms/ControlEvent?ref=angularspace.com) should be emitted or not. Methods like [markAsTouched](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#markAsTouched), [markAllAsTouched](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#markallastouched) (for groups and arrays), [markAsDirty](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#markAsDirty), [markAsPending](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#markaspending), [markAsPristine](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#markaspristine), and [markAsUntouched](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#markasuntouched) can be called with the option’s `emitEvent` property. If those methods are called without it, or `emitEvent` is set to `true`, then the [ControlEvents](https://angular.dev/api/forms/ControlEvent?ref=angularspace.com) will be emitted. Otherwise, they will not. But there is a trick. It does not mean the state of the control will not be changed! It will be. The only difference here is that the event will not be triggered. Just take a look at the code below: ```typescript const control = new FormControl("Lorem ipsum"); // control.touched -> false control.markAsTouched({ emitEvent: false }); // control.touched -> true ``` The value of the touched property has been changed anyway. ### Value and state events of a group of controls So far we have covered [ControlEvents](https://angular.dev/api/forms/ControlEvent?ref=angularspace.com) in terms of [FormControl](https://v17.angular.io/api/forms/FormControl?ref=angularspace.com#description). It’s time to talk about more advanced structures like groups, and arrays. As we all know, all of those classes inherit from AbstractControl. It means all of them will emit [ControlEvents](https://angular.dev/api/forms/ControlEvent?ref=angularspace.com). When it comes to groups and arrays, the [ControlEvents](https://angular.dev/api/forms/ControlEvent?ref=angularspace.com) object reveals its true power. Until version 18 we had access to the newly emitted value or state of the group/array, but we didn’t know which control was the origin of the change. Now, we can take advantage of [ControlEvents](https://angular.dev/api/forms/ControlEvent?ref=angularspace.com) and target a specific control inside the subscription. It’s super useful when we want to react to a particular control change. Let’s dive into an example: ```typescript import { Component, OnInit } from "@angular/core"; import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms"; @Component({ selector: "app-form-group-events", standalone: true, imports: [ReactiveFormsModule], template: `




`, }) export class FormGroupEventsComponent implements OnInit { form = new FormGroup({ name: new FormControl("John"), surname: new FormControl("Doe"), age: new FormControl(30), }); ngOnInit(): void { this.form.events.subscribe((event) => { console.log(event); }); this.form.get("name")?.setValue("Jane"); } } ``` As a result, we’ll get two events emitted: [ValueChangeEvent](https://angular.dev/api/forms/ValueChangeEvent?ref=angularspace.com) and [StatusChangeEvent](https://angular.dev/api/forms/StatusChangeEvent?ref=angularspace.com). Both provide an event object that contains a source field, which points directly to the changed control. The second field depends on the event type, and it can be a value of the control/group or the value of its state. The tricky part is that the source field always points to the control that value/state has been changed. The second field is always related to the object we are subscribing to. It’s even more vivid when we work with more advanced structures. The example below shows a form with nested [FormGroup](https://angular.dev/api/forms/FormGroup?tab=description&ref=angularspace.com) objects. It also contains four subscriptions: to the form root, to the `address` field, and to the `street` and `city` fields within the `address`. ```diff import { Component, OnInit } from "@angular/core"; import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms"; @Component({ selector: "app-form-group-events", standalone: true, imports: [ReactiveFormsModule], template: `




+ +

+ +
+ Address: + + + + +

+ + + +
`, }) export class FormGroupEventsComponent implements OnInit { form = new FormGroup({ name: new FormControl("John"), surname: new FormControl("Doe"), age: new FormControl(30), + address: new FormGroup({ + street: new FormControl("Collins Street"), + city: new FormControl("Fox River"), + }), }); ngOnInit(): void { this.form.get("address.street")?.events.subscribe((event) => { console.log(event); }); + this.form.get("address.city")?.events.subscribe((event) => { + console.log(event); + }); + + this.form.get("address")?.events.subscribe((event) => { + console.log(event); + }); + + this.form.events.subscribe((event) => { + console.log(event); + }); } } ``` Changing the value of the `city` field will trigger events emitter assigned to the `city` field and all its parents (yes, we can prevent this propagation within the `onlySelf` option passed to [setValue](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#setValue) method, but it's not a case here). The image below shows how [ValueChangeEvent](https://angular.dev/api/forms/ValueChangeEvent?ref=angularspace.com) is propagated through the tree of the form, and what data is put into the event object on each level of the form. ![Schema of events propagation](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/image-3.png) ### Submit and reset events So far we have covered how the events field behaves in terms of a single [FormControl](https://v17.angular.io/api/forms/FormControl?ref=angularspace.com#description) and structures like [FormGroup](https://angular.dev/api/forms/FormGroup?tab=description&ref=angularspace.com) values and states. But that’s not all. Unified Control State Change Events brings long-awaited features, which are submit and reset events. It means no more workarounds, no more submit and reset buttons listeners! Let’s add this functionality to our example. ```diff ... @Component({ selector: 'app-form-group-events', standalone: true, imports: [ReactiveFormsModule], template: `






Address:

+ +
+ + +   +
`, }) ``` Our form is complete now: ![The view of the example form](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/image-4.png) Let’s trigger the submit event by clicking on the Submit button, and see what the console logged. Unlike events, we have been talking about before, [FormSubmittedEvent](https://angular.dev/api/forms/FormSubmittedEvent?ref=angularspace.com) contains **only** source property, which points to the main form object. To be more precise, it points to the object assigned to the `formGroup` directive: `
`. Two things need to be explained in detail. First, the emission of the [FormSubmittedEvent](https://angular.dev/api/forms/FormSubmittedEvent?ref=angularspace.com) is not related to the actual state of the form. It means the event will be emitted every time we click on the submit button, regardless of whether the form has already been submitted. The second thing is the origin of [FormSubmittedEvent](https://angular.dev/api/forms/FormSubmittedEvent?ref=angularspace.com). It gets triggered in response to the native forms [submit](https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent?ref=angularspace.com) event. It means the [](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form?ref=angularspace.com) element has to contain a submit button. The button can be defined explicitly by setting the `type` attribute to `'submit'`, or it can be a button without a type ([https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button?ref=angularspace.com#type)). The last event I’m going to cover is [FormResetEvent](https://angular.dev/api/forms/FormResetEvent?ref=angularspace.com). As the name implies, this event is emitted every time the form is reset. Similar to [FormSubmittedEvent](https://angular.dev/api/forms/FormSubmittedEvent?ref=angularspace.com), the [FormResetEvent](https://angular.dev/api/forms/FormResetEvent?ref=angularspace.com) object contains only the source property. What is interesting is that the reset process is a little bit more complex. By resetting we assume all of the control values and states are going to be set (or revert if you prefer) to its initial values. It means every control (and groups of course) is going to emit three events: [TouchedChangeEvent](https://angular.dev/api/forms/TouchedChangeEvent?ref=angularspace.com), [ValueChangeEvent](https://angular.dev/api/forms/ValueChangeEvent?ref=angularspace.com), and [StatusChangeEvent](https://angular.dev/api/forms/StatusChangeEvent?ref=angularspace.com). The form root object will emit one more event - [PristineChangeEvent](https://angular.dev/api/forms/PristineChangeEvent?ref=angularspace.com), to ensure that the reset process is completed. After that, the root object will emit the [FormResetEvent](https://angular.dev/api/forms/FormResetEvent?ref=angularspace.com) event. \` Assuming, we have two subscriptions (all the rest are not necessary for this example), one to the address field, and the second one to the root object, we can observe the entire process of the form resetting. Let’s take a look at the image below. The first section of events is related to the address field, whilst the second one is related to the main form object. ![List of emitted events triggered from the view](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/image-5.png) The last thing worth mentioning is that the [FormResetEvent](https://angular.dev/api/forms/FormResetEvent?ref=angularspace.com) (at the time of writing this article) is emitted only when we reset the form from the view. Calling the [reset](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#reset) method will trigger all events, but reset: ![List of emitted events triggered from the forms API](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/image-6.png) ### Getting the event we want The event object emits lots of events every time we change a value. For most of the cases, we are willing to listen only for one of two event types. For this we can embrace the rxjs [filter](https://rxjs.dev/api/operators/filter?ref=angularspace.com) operator to filter out events we are not interested in, and focus only on the desired, like [ValueChangeEvent](https://angular.dev/api/forms/ValueChangeEvent?ref=angularspace.com). The code snippet demonstrates this operation in practice. ```typescript form.events .pipe(filter((event) => event instanceof ValueChangeEvent)) .subscribe((event) => { console.log(event); }); ``` ### Conclusion Let's summarize what we have learned today. Control State Change Events were introduced in Angular v18\. The main purpose of it is to unify events emitted by the form controls. The benefits of using them are: - There’s no need to create (and maintain) two subscriptions, one to [valueChange](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#valueChanges), and the second to [statusChanges](https://v17.angular.io/api/forms/AbstractControl?ref=angularspace.com#statusChanges) - You can react when a user starts interaction with the form, by making it dirty and touched, as pristine and touched status can be observed. - From now on, it’s not necessary to listen to the submit and reset buttons clicks, forms API allows us to work with those events in a reactive way. - You have access to the control that is a source of the change --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/Screenshot-2024-06-26-at-00.40.21.png) ### 10x e-Book Giveaway! - Complete Guide To Qwik by Giorgio Boa URL: https://www.angularspace.com/10x-e-book-giveaway-complete-guide-to-qwik-by-giorgio-boa/ Last updated: 2024-06-25T16:14:37.000Z Get ready for new giveaway! This time something from Qwik friends! [Giorgio Boa](https://x.com/giorgio%5Fboa?ref=angularspace.com) wrote a fantastic book about Qwik. Book full retail price is set at $79. > Build instantly-interactive apps without effort with Qwik. > > The Complete Guide to Qwik is a in-depth learning path, designed to help you master this powerful framework. Through practical examples and detailed analysis of how Qwik functions, you will learn to create your own large-scale, instant applications. I have 10 copies to giveaway in a raffle. 5 copies are going to land in ⁠Discord Shop. Here you can see all the details about the book + get first chapter for free. [https://www.newline.co/courses/complete-guide-to-qwik](https://www.newline.co/courses/complete-guide-to-qwik?ref=angularspace.com) Giveaway is now LIVE!! 🥳 Instructions on how to participate in giveaway below 👇 _This post is for subscribers only._ ### Event management on steroids URL: https://www.angularspace.com/event-management-on-steroids/ Last updated: 2024-06-25T23:21:53.000Z This will be an educational article, showcasing one of the cooler Angular features that not many people pay enough attention to. But it will also be a shameless plug of our [open source](https://github.com/taiga-family/ng-event-plugins?ref=angularspace.com) library because you might not know you want it, although I'm sure you do. For a mere 1kB gzip it will improve your DX across many different scenarios, which we will explore here. If you know this library already, don't worry, there are some new features I'll announce here. What is event management in Angular? It is what the framework does when you write `(click)` in your template. Have you ever wondered, what magic goes into listening to an Escape key with `(keydown.esc)`? In this article we will dive a bit into the source code to explore this lesser known public API and how we can leverage it for our benefit. ## EventManager In Angular, templates are rendered with what is called a Renderer. We will not go deep into how it operates, we will just take a look at this little method: ```JS listen( target: 'window' | 'document' | 'body' | any, event: string, callback: (event: any) => boolean, ): () => void { (typeof ngDevMode === 'undefined' || ngDevMode) && this.throwOnSyntheticProps && checkNoSyntheticProp(event, 'listener'); if (typeof target === 'string') { target = getDOM().getGlobalEventTarget(this.doc, target); if (!target) { throw new Error(`Unsupported event target ${target} for event ${event}`); } } return this.eventManager.addEventListener( target, event, this.decoratePreventDefault(callback), ) as VoidFunction; } ``` When you write `(window:resize)` or `(keydown.esc)` this method gets called. In the first case, the target is going to be `window` string, in second — it will be the element you write this listener on. As you can see, after resolving the actual target from string all it does is delegate the event listening to `eventManager`. What is this mysterious beast? It is a global Angular service that has this one relevant method: ```JS addEventListener( element: HTMLElement, eventName: string, handler: Function ): Function { const plugin = this._findPluginFor(eventName); return plugin.addEventListener(element, eventName, handler); } ``` It is a one-liner that gets a target, event name (as you typed it, for example `keydown.esc`) and a callback. It returns a cleanup function for Renderer to call when the element is destroyed. By now you can see that the meat of this mechanism is in plugins. What are they? Let's find out. ## EventManagerPlugin While the list of available plugins is provided with a public token `EVENT_MANAGER_PLUGINS` the abstract class itself became public only in Angular 17\. This doesn't mean we cannot leverage this in earlier versions, but it's just easier in 17+ in terms of types. The interface is pretty simple though. If we want to add our own plugin, all we need to implement is 2 methods: ```JS abstract supports(eventName: string): boolean; abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function; ``` What plugins do we have out of the box? There are 3: 1. `DomEventsPlugin` — this is your one size fits all guy. It's a fallback plugin that just uses native `addEventListener` with the given event name as is. 2. `KeyEventsPlugin` — this plugin is responsible for `(keydown.esc)` kind of events. It listens to all keydown events outside of zone.js so that change detection is not triggered for irrelevant key presses. Then if the key matches `esc` it triggers callback inside zone.js. 3. `HammerGesturesPlugin` — that plugin is optional and is enabled if you add HammerModule. It simplifies using Hammer.js gesture events for touch interactions. Since `EVENT_MANAGER_PLUGINS` is a multi token, nothing is stopping us from extending the set with our own plugins, just like `HammerModule` does. Doing so is easy. Let's create our first specimen to get a taste of what we can achieve and how. ## Writing our own plugins How often do you pass `$event` object to callback just to call `.stopPropagation()`? If you work with DOM a lot, like me, you probably did so here and there. Wouldn't it be nice if we could just declaratively write `(click.stop)` and have Angular take care of it for us? It's really easy to achieve with plugins. Have a look: ```JS export class StopEventPlugin extends EventManagerPlugin { supports(eventName: string): boolean { return eventName.split('.').includes('stop'); } addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { const wrapped = (event: Event) => { event.stopPropagation(); handler(event); } return this.manager.addEventListener(element, eventName.replace('.stop', ''), wrapped) } } ``` Then we can add this constant to providers in our app bootstrap so that this plugin becomes available for `EventManager`: ```JS export const STOP_PLUGIN = { provide: EVENT_MANAGER_PLUGINS, multi: true, useClass: StopEventPlugin, }; ``` So what have we done here? We investigated the event name for `.stop` block and reported that our plugin supports an event if it has that modifier. Then in the `addEventListener` function we did 3 things: 1. Dropped that modifier from the name. 2. Added a little arrow function wrapper that calls `.stopPropagation()` on the event, before passing it on to the original callback. 3. Passed this data back to `EventManager` so it would determine the right plugin for this particular event This is a very non-intrusive way to extend Angular logic. We barely wrote any code, so we most likely will not introduce potential bugs. And we also just plugged our logic inside the existing mechanism. That means we can write something like `(keydown.esc.stop)` and it would still work as expected. Very handy if you want to close a dropdown but keep a modal dialog with it open. One awesome feature that WebStorm has in this regard is [Web Types](https://plugins.jetbrains.com/docs/intellij/websymbols-web-types.html?ref=angularspace.com). They allow you to extend autocomplete and type check your custom events against a JSON schema, so you can have this: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/autocomplete.png) And then your callback will still know that `$event` is `MouseEvent`. The list above foreshadows what we will talk about next. You probably guessed that we can also create a similar plugin to call `.preventDefault()`, but this is just a tip of the iceberg. This simple API opens up many doors and for some time we've been gathering all the helpful plugins under one little library. Let's see what we have so far. ## [@taiga-ui/event-plugins](https://github.com/taiga-family/ng-event-plugins?ref=angularspace.com) Recently we released the next major version of our library to tie-in with [Taiga UI](https://taiga-ui.dev/?ref=angularspace.com) 4\. It has a few new features, as well as a bumped Angular version and a slight refactor here and there. Besides already mentioned `.stop` and `.prevent` plugins, what else do we have ready for you? ### SilentEventPlugin Remember how built-in `KeyEventsPlugin` exits the zone.js for irrelevant keys so it doesn't trigger change detection? While we all patiently wait for stable zoneless Angular you might also want to be able to execute some callbacks without triggering change detection. For example, if you do not want to move focus away on click — you can achieve this with the following line in `host` of your component/directive: `'(mousedown.prevent.silent)': '0'`. It prevents the default behavior of `mousedown` event, which is to move the focus, and does so outside on `NgZone`. 0 is just the shortest possible empty callback. Most of those plugins can be combined like that. ### SelfEventPlugin Sometimes you want to ignore bubbled events. You would typically do it by checking if `currentTarget` equals the `target` of the event in question. It's easy to do using the same technique of callback wrapping described above. With this plugin just write `(transitionend.self)` and be sure you will not react to some nested DOM element transition that you might not even have control of. ### OptionsEventPlugin This is one of the most important plugins in the set, as it does not just improve DX, but also brings a feature previously not possible with traditional Angular event listeners. You know you can pass **options** object as the last argument for [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener?ref=angularspace.com#options)? Most importantly it allows you to listen to the events in the capturing phase. [Read more](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building%5Fblocks/Events?ref=angularspace.com#event%5Fcapture) about it if you are not familiar with the term. Basically, it is the opposite of bubbling and goes from top to bottom of the DOM tree. Not only it allows you to listen to events from child nodes that do not bubble, it also serves as a *"first responder"* callback as it will be triggered before all others. This is a really helpful feature giving you some control over the order of execution. ### ResizeEventPlugin **(new)** We all know there's a `resize` event on `window`, but what if we want to listen to size changes of a DOM element? There's a tool for that, called [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver?ref=angularspace.com). While we have [Web APIs for Angular](https://github.com/taiga-family/ng-web-apis?ref=angularspace.com), our open source initiative to bring Web APIs into Angular in idiomatic form, it still requires you to import a directive that uses an observer under the hood. Event plugin, on the other hand, is added only once, to the global providers, and after that you are able to just write `(resize)` on any element and be notified if its size changes. ### GlobalEventPlugin **(new)** Remember in the first code snippet from Angular sources we saw that it resolved global objects, such as `window`, `document` or `body`? But these are not the only global objects that implement the `EventTarget` interface. For example, you might want to detect screen keyboard or rotation with resize of [visualViewport](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport?ref=angularspace.com). With this plugin you can write `(visualViewport>resize)` similar to built-in solution, except `>` instead of `:`, and it will try to find that global object on `globalThis`. ## Putting it to good use! Now that we have all those tools ready let's see what we can achieve with them in a simple app with 2 components. Imagine we have a form with an input that automatically grows in size and a submit button. While we patiently wait for [field-sizing](https://developer.mozilla.org/en-US/docs/Web/CSS/field-sizing?ref=angularspace.com) to ship, we can implement such an input using a little trick. We will make the actual input invisible and absolutely positioned and show text underneath in a `span`. Take a look at this demo and let's explore each use of our new plugins: First of all, we have `(mousedown.prevent.silent): "0"` on the submit button. We do this so that when we tap the button on mobile, focus does not leave the input. Otherwise the screen keyboard will disappear, layout will shift and click event might not even happen. At the time of writing, this issue can be seen on Airbnb chat web app. Second case for plugins is `(click.capture.silent)` inside our button. We use capturing phase to react to this event first. If our button is in loading state we stop propagation of the `click` event. This stops our form from being submitted multiple times while we wait for the response. You might ask why not just disable the button? This solution has accessibility concerns. Disabled button will lose focus and will not indicate to the screen reader what is going on. In our implementation it would read out loud that the button label is "Loading" and we can also use `aria-disabled` to let user know that this button does not do anything at the moment. Lastly, in our auto-grow input, when we type enough text for it to overflow — native `input` will start to scroll. We need to compensate for its scroll position in our `span`, which is easy to do using `text-indent` CSS, because it can be negative. However, `scroll` event does not bubble, so there is no way for us to listen to it in the parent. This is a great use for `.capture` event plugin because it allows us to react to this event in capturing phase, when it propagates from top to bottom, even for events that do not bubble. With this little example you can see that custom event plugins can improve your DX. They allow you to write concise declarative statements for common situations, such as preventing default event behaviors. But they also can achieve in one line what otherwise was not that easy to do in Angular, such as listening to events in capturing phase which can come in really handy! ## Conclusion Event management in Angular, as usual, is something we can augment with Dependency Injection — arguably the best part of the framework. With the introduction of `EventManagerPlugin` abstract class to public API it has now become first class citizen, and it's a good idea to familiarize yourself with the mechanism. [@taiga-ui/event-plugins](https://github.com/taiga-family/ng-event-plugins?ref=angularspace.com) is a good starting point, with several ready-to-use quality of life improvements. But it is definitely not exhaustive. You might come up with a clever and ergonomic solution for your particular needs. And if you think your idea could be helpful to others — it's a good reason to contribute to our library. Similarly, if you have a solid idea, but you are unsure how to implement it best — a feature request could bring in collective mind and help develop the library further. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/Screenshot-2024-06-24-at-15.46.39.png) ### Brewing Bootleg Reactive Forms Signals from RXJS URL: https://www.angularspace.com/brewing-bootleg-reactive-forms-signals-from-rxjs/ Last updated: 2024-06-20T20:02:27.000Z Why wait for a formal reactive forms and signals integration when you can brew one up yourself? If you take a recipe handed down by one of the finest brewers in the reactive form space (Joshua Morony) and throw in some extra ingredients of you own, you can utilize signals with reactive forms today. With some simple RxJS to signal interop methods, you can react to value and status state in reactive forms using signals as early as Angular 16! And with a new API for form events introduced in Angular 18, this future facing approach will only get better with age. ## A Sample of the End Product Should you follow the recipe right, a function with this signature is the end product. ```JAVASCRIPT // Disclaimer: T is most often inferred as Partial in practice due to a limitation of form typing // See more, and how in the future this utility could be enhanced further // https://github.com/ngxtension/ngxtension-platform/pull/391#issuecomment-2163512231 type FormEventData = { // These values are possible in Angular 16, see links at the bottom value: T; status: FormControlStatus; valid: boolean; invalid: boolean; pending: boolean; // These values are possible as of Angular 18 touched: boolean; pristine: boolean; dirty: boolean; untouched: boolean; }; function allEventsObservable(form: AbstractControl): Observable> function allEventsSignal(form: AbstractControl): Signal> ``` Return signature ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/Intro-Forms.gif) Preview of return signature with live values from a form control ## Familiar Ingredients And Process This wouldn't be a recipe from the internet if there wasn't some backstory given. BUT IT'S IMPORTANT I SWEAR! Some context on how this was done in the old days will give insight into this new API for reacting to form events. The inspiration for this recipe? A section of the video ["SIGNALS can make Angular "REACTIVE" forms more reactive"](https://www.youtube.com/watch?v=cxoew5rmwFM&t=211s&ref=angularspace.com) by Joshua Morony, a master brewer of observability himself. The whole approach of the video extends beyond the scope of this article, but they key helper function lies within at [3:31](https://www.youtube.com/watch?v=cxoew5rmwFM&t=211s&ref=angularspace.com), with the function `formValues(this.form)`. ```JAVASCRIPT // https://github.com/joshuamorony/signal-slice-forms/blob/main/src/app/shared/utils/signal-forms.ts#L5 import { FormGroup } from '@angular/forms'; import { map } from 'rxjs/operators'; export function formValues(form: FormGroup) { return form.valueChanges.pipe(map(() => form.getRawValue())); } ``` This recipe has proven to be timeless, even with new ingredients possible in Angular 18\. From the idea of making utilities to react to form changes with `form.valueChanges` and `form.statusChanges`, we can brew form signals. But before we get started, we need to tweak the base a bit. An essential part of these utilities is that not only can they grab the value and status as they change, but also have initial values. Josh's full approach has an initial form state done his way, but with our recipe we can always ensure the value stream has a value with the RXJS operator `startWith`. And as a bonus, we will throw in a `distinctUntilChanged` to help reduce some redundant emissions from the value stream, and a generic `T` for a stronger type. ## Full Recipe: Retrieving `value/status/touched/pristine` as a Signal and Observable using a new Angular 18 API The pull request that made it into Angular 18 called ["Unified Control State Change Events #54579"](https://github.com/angular/angular/pull/54579?ref=angularspace.com) introduced an observable on forms called `events` which exposes a stream of events and their respective values. For a detailed overview of how it works, check out this incredible short video by Igor Sedov, ["New in Angular 18: Unified Control State Change Events for Forms"](https://www.youtube.com/watch?v=v7r-7PHaEtY&ref=angularspace.com). Events (which `extend ControlEvent`) - `ValueChangeEvent` - `StatusChangeEvent` - `TouchedChangeEvent` - `PristineChangeEvent` - `FormResetEvent` (no value accessor) - `FormSubmittedEvent` (no value accessor) This recipe will not include helpers to get a form's `reset` and `submitted` as they do not have initial values. A recipe with those would have its own considerations that can be left to aspiring brewers. ### Ingredients - `form.events` for value/status/touched/pristine events - Basic RXJS: (`pipe/map/startWith/combineLatest`) - Key ingredient: RXJS & Signal interop: `toSignal()` ### First, let's make some similar looking helper functions We can filter for `ValueChangeEvent` instances from the `events` stream. ```JAVASCRIPT import { AbstractControl, ControlEvent, ValueChangeEvent } from '@angular/forms'; function valueEvents(form: AbstractControl): Observable> { return form.events.pipe( filter( (event: ControlEvent): event is ValueChangeEvent => event instanceof ValueChangeEvent, ), ); } ``` For brevity, the same can be done for `StatusChangeEvent`. Additionally, this new `event` object also exposes `TouchedChangeEvent` and `PristineChangeEvent`. And among these four event types, they also return their respective types of values synchronously, such as `touched: boolean` for `TouchedChangeEvent`. One more type of helper functions: `isType` functions. Since the streams will return various events like `StatusChangeEvent` and we want that event's `status`, but we also use `startWith(form.status)`, we will want type helpers to differentiate the stream values we map out. Inside the body of our combination function, we will use them like this: ```JAVASCRIPT function isStatusEvent(event: ControlEvent | T): event is StatusChangeEvent { return event instanceof StatusChangeEvent; } map(([valueParam, statusParam, touchedParam, pristineParam]) => { ... // This can be turned into a ternary let stat: FormControlStatus | StatusChangeEvent; if (isStatusEvent(statusParam)) { stat = statusParam.status; } else { stat = statusParam; } ... }) ``` The full code for all of the `isType` and `typeEvents` for the four types of events will be linked to at the end. Oh, and one more thing I have... put off until now. Put another way, I should no longer `defer` explaining one thing. If the form had its `value` or other data changed in a lifecycle hook like `OnInit`, then that happens after the `startWith` usages. So those form data setters would be missed, and not reflected in the form's initial subscription state. Luckily, as described in ["Angular FormGroup valueChanges: Deferring Observable"](https://fullstackbuff.dev/stories/defer-form-value-changes?ref=angularspace.com), we can just wrap this whole thing in an RXJS `defer` and call it a day! With all of the ingredients and helper functions, let's look at the final recipe: ```JAVASCRIPT export function allEventsObservable( form: AbstractControl, ): Observable> { return defer(() => combineLatest([ valueEvents$(form).pipe( startWith(form.value), map((value) => (isValueEvent(value) ? value.value : value)), distinctUntilChanged( (previous, current) => JSON.stringify(previous) === JSON.stringify(current), ), ), statusEvents$(form).pipe(startWith(form.status)), touchedEvents$(form).pipe(startWith(form.touched)), pristineEvents$(form).pipe(startWith(form.pristine)), ]).pipe( map(([valueParam, statusParam, touchedParam, pristineParam]) => { // Original values (plus value) const stat: FormControlStatus | StatusChangeEvent = isStatusEvent(statusParam) ? statusParam.status : statusParam; const touch: boolean | TouchedChangeEvent = isTouchedEvent(touchedParam) ? touchedParam.touched : touchedParam; const prist: boolean | PristineChangeEvent = isPristineEvent(pristineParam) ? pristineParam.pristine : pristineParam; // Derived values - not directly named as events, // but are aliases for something that can be derived from original values const validDerived = stat === 'VALID'; const invalidDerived = stat === 'INVALID'; const pendingDerived = stat === 'PENDING'; const dirtyDerived = !prist; const untouchedDerived = !touch; return { value: valueParam, status: stat, touched: touch, pristine: prist, valid: validDerived, invalid: invalidDerived, pending: pendingDerived, dirty: dirtyDerived, untouched: untouchedDerived, }; }), )); } export function allEventsSignal( form: AbstractControl, ): Signal> { return toSignal(allEventsObservable(form), { initialValue: { value: form.value, status: form.status, pristine: form.pristine, touched: form.touched, valid: form.valid, invalid: form.invalid, pending: form.pending, dirty: form.dirty, untouched: form.untouched, }, }); } ``` This is how we use it in the component: ```JAVASCRIPT fb = inject(NonNullableFormBuilder); form = this.fb.group({ firstName: this.fb.control('', Validators.required), lastName: this.fb.control(''), }); formEventsAsObservable = allEventsObservable(this.form); formEventsAsSignal = allEventsSignal(this.form); ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/V18-Forms.gif) Form Events util in action with a form and the util object as JSON ## Summary With the right ingredients, a signals API for forms has been brewing since v16 and got even better in v18\. Until a formal integration of reactive forms and signals is introduced, this recipe is about as good as it gets. And I hope above all, even if you don't need to bootleg reactive form signals in the future, you have learned something about forms, signals, observables, and the observable & signal interoperability package. *Thank you Erick Rodriguez, Alain Boudard, and Matthieu Riegler for peer reviews, and Angular Spaces for the opportunity.* ## Links - Stackblitz example with working v16 and v18 examples: [https://stackblitz.com/edit/stackblitz-starters-masfsq?file=src%2Fform-events-utils.ts](https://stackblitz.com/edit/stackblitz-starters-masfsq?file=src%2Fform-events-utils.ts&ref=angularspace.com) - v18 output shown inline in the HTML - v16 output logged to console - Repo with full utility code and examples - [Home page of example](https://github.com/michael-small/form-event-signals-article/blob/main/src/app/app.component.ts?ref=angularspace.com). Run `ng serve` and navigate to `http://localhost:4200/` - [v16 utils](https://github.com/michael-small/form-event-signals-article/blob/main/src/app/v16-utils.ts?ref=angularspace.com) - The example repo uses v18, but the exact code from this util file can be extracted to a v16 repo, or even earlier for just the observable stream - Check the console while running to see how those events are fired off - [v18 utils](https://github.com/michael-small/form-event-signals-article/blob/main/src/app/form-events.ts?ref=angularspace.com) - All four types of event values are shown in the template --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/Screenshot-2024-06-20-at-22.01.46.png) ### 10x e-Book Giveaway! - Modern Angular by Armen Vardanyan URL: https://www.angularspace.com/e-book-modern-angular-by-armen-vardanyan/ Last updated: 2024-06-11T11:17:35.000Z It was a long time since we had a giveaway :). Today [**GDE Armen Vardanyan**](https://x.com/Armandotrue?ref=angularspace.com) has something special for you. You can win **one of 10 copies** of his first ever e-book! We have organized this cooperating with Manning Publications. Modern Angular - sounds very interesting isn’t it? > *Modern Angular* gets you rapidly up to speed with Angular’s latest innovations. *Modern Angular* is full of the up-to-date knowledge you need to build highly effective web frontends. Hear it from Armen himself! 0:00 /3:24 1× Learn more about e-book here: [https://mng.bz/Y7yQ](https://mng.bz/Y7yQ?ref=angularspace.com) Instructions on how to participate in giveaway below 👇 _This post is for subscribers only._ ### Injecting content programmatically in Angular URL: https://www.angularspace.com/injecting-programatically-content-in-angular/ Last updated: 2024-06-10T17:22:07.000Z In this article, you will learn the essentials of working with dynamic component injection as a part of the logic of the component. Before you start, a replica of this work is available in this [link](https://stackblitz.com/edit/stackblitz-starters-jdqjrl?file=src%2Fmain.ts&ref=angularspace.com) in stackblitz. ## Injecting an arbitrary component inside of a component One of the most fascinating features Angular offers is related to component injection. Component injection is a technique that allows a component of your choice to be injected inside another component. This is particularly valuable as modern websites are often composed in such a way that allows for this flexibility. ## How I do that? **Recommended Approach for Angular 17+ projects** - If you are using a [Standalone Component](https://angular.dev/guide/components/importing?ref=angularspace.com#standalone-components), your components have to be declared in your **imports** section: ```javascript // If I were to use this component to be dynamically imported along other compoents @Component({ standalone: true, selector: 'profile-photo', }) export class ProfilePhoto { } @Component({ standalone: true, imports: [ProfilePhoto], //then this is the place where it should be declared templateUrl: 'userProfile.component.html' }) export class UserProfile { } ``` Important note here: If you are using standalone components, all the other components in the operation, needs to be **standalone** as well. Importing a component that is not standalone will require you to import the respective [module](https://angular.dev/guide/ngmodules?ref=angularspace.com) that includes the declaration of such component. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/image-2.png) If a non-standalone component is directly imported into a standalone component, it will show this message. ## Implementation Now that we know the basics, let's create a small project where we will dynamically inject two components inside a component template: - The UI should show two buttons, each one calling the respective component into the DOM. - The respective component should follow the DOM flow, avoiding overlap or deleting previously injected components. - Each component, when clicked, should remove itself. ## Composition Injecting a component is probably the easiest job inside the context of a component; however, the component will be placed arbitrarily at the end of its body. If you want to place things in the right place, it is advised to use a [ng-container](https://angular.io/api/core/ng-container?ref=angularspace.com) to ensure the right area where the components will be injected into the view. First, let's create our host component where we will do most of the work. There are diverse ways to project content into a component, but I find using [ViewContainerRef](https://angular.dev/guide/components/importing?ref=angularspace.com#standalone-components)to be the simplest way to achieve this approach. Let's create an Angular component that will be the host of our application, with two buttons: ```js import {Component, ViewContainerRef, inject} from "@angular/core"; @Component({ standalone: true, selector:'app-root', template: ` ` }) export class AppComponent { private viewContainerRef = inject(ViewContainerRef); public injectC1(){ //inject component1 } public injectC2() { // inject component2 } } ``` So far, so good. We defined a private `viewContainerRef` that will ensure any component can be injected into the component that invokes it. Now let's create two child components that will be injected into the component, and make the buttons execute the injection ```javascript import {Component, ViewContainerRef, inject} from "@angular/core"; @Component({ standalone: true, selector:'app-component1', styleUrl :'c1.css', template: `
This is Component 1
` }) export class Component1 {} @Component({ standalone: true, selector:'app-component2', styleUrl :'c2.css', template: `
This is Component 2
` }) export class Component2 {} ``` In addition, we add the injection of components on the desired functions: ```diff @Component({ ... }) export class AppComponent { private viewContainerRef = inject(ViewContainerRef); public injectC1(){ + this.viewContainerRef.createComponent(Component1) } public injectC2() { + this.viewContainerRef.createComponent(Component2) } } ``` finally, let's create the stylesheets for each component: | main.css | | -------- | ```css .grid { margin-top:5px; display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:5px; } .container { border: 1px solid green; } ``` | c1.css | | ------ | ```css .c1{ background-color:#FF0000; color:#FFF; } ``` | c2.css | | ------ | ```css .c2{ background-color:#0000FF; color:#FFF; } ``` The result will look like this. Note that, from the perspective of the component, injected components without a specific container will be placed *`at the end`* of the invoking component, which is **AppComponent**. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/projection1-2.gif) **Injected components will be placed at the end of the AppComponent template* However, as a demo, it works nicely, but what if we have to inject components in a specific area of the page? Let's create in the AppComponent two columns in a grid where one is static and one is supposed to be the target of our injected components: ```diff @Component({ ... template: ` +
+
this is static
+
this is dynamic
+
` }) export class AppComponent { ... } ``` ## Using ng-container To determine the target, we might want to use an [ng-container](https://angular.dev/api/core/ng-container?ref=angularspace.com) with a name on it in our AppComponent, so we can use it to programatically inject content in such space. We will use [@ViewChild](https://angular.dev/api/core/ViewChild?ref=angularspace.com) to target the right space named by the [ng-container](https://angular.dev/api/core/ng-container?ref=angularspace.com). ```diff @Component({ ... , template: `
this is static
+
` }) export class AppComponent { - private viewContainerRef = inject(ViewContainerRef); + @ViewChild('targetSpace', { + read: ViewContainerRef + }) + private targetSpace!:ViewContainerRef; public injectC1(){ + this.targetSpace.createComponent(Component1) - this.viewContainerRef.createComponent(Component1) } public injectC2() { + this.targetSpace.createComponent(Component2) - this.viewContainerRef.createComponent(Component2) } } ``` The resultant code now injects into the [ng-container](https://angular.dev/api/core/ng-container?ref=angularspace.com) `#targetSpace` the components programatically: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/projection2.gif) Injected components will now be targeted on the ng-container #targetSpace ## Clicked components remove themselves from the DOM To achieve this, we need to trigger an event, and for that, we will use the function [output](https://angular.dev/guide/components/output-fn?ref=angularspace.com). You probably saw [@Output](https://angular.dev/api/core/Output?ref=angularspace.com), which is a decorator and has the same result as `output`. When the component is clicked, we will emit the event through the [output](https://angular.dev/guide/components/output-fn?ref=angularspace.com), so the parent component can resolve accordingly. For `Component1` and `Component2` we will create the respective output events and the event handler that will trigger the behavior to emit the click event. As `clicked` is public, we can execute the emit event on the template using `clicked.emit()` ```diff + import {Component, ViewChild, ViewContainerRef, output} from "@angular/core"; @Component({ ... template: ` -
This is Component 1
+
This is Component 1
` }) export class Component1 { + public clicked = output() } @Component({ ... template: ` -
This is Component 2
+
This is Component 2
` }) export class Component2 { + public clicked = output() } ``` To keep the logic of removing the element on the parent element, we need to call the reference of the created component so when clicked, it can be removed. Output events are subscribable, so in `AppComponent` you want to use the `subscribe` method for the instance of the component event (in this case `clicked` for each injected component). Each time a component is injected, a reference in memory keeps a pointer to that component. This ensures that each component occupies a specific space in memory, eliminating the risk of closing the wrong component. Understanding the concept of a `reference` and `instance` in JavaScript/TypeScript is crucial, especially in complex cases, as it helps to understand how to manage and access these components effectively.. ```diff @Component({ ... }) export class AppComponent { ... public injectC1(){ - this.targetSpace.createComponent(Component1) + const componentRef = this.targetSpace.createComponent(Component1) + componentRef.instance.clicked + .subscribe( () => { + componentRef.destroy(); + }) } public injectC2() { - this.targetSpace.createComponent(Component2) + const componentRef = this.targetSpace.createComponent(Component2) + componentRef.instance.clicked + .subscribe( () => { + componentRef.destroy(); + }) } } ``` Voila! now each component, when clicked, will remove itself: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/projection3-2.gif) Injected components when clicked will be removed by itself ## Handling component destruction events As we dinamycally inject components, and destroy them on the fly, we should ask ourselves what should happen when a component is destroyed. We can use some of the new features of Angular 17 to make this task a less daunting one. ### TakeUntilDestroyed, OutputToObservable and DestroyRef implementation With [TakeUntilDestroy](https://angular.dev/api/core/rxjs-interop/takeUntilDestroyed?ref=angularspace.com) along with [OutputToObservable](https://angular.dev/guide/components/output-fn?ref=angularspace.com#converting-an-output-to-an-observable) will help with the task of converting our `output` event into an observable one. However, we need to first ensure that a Destroy Reference is available on each component. For this we will use [DestroyRef](https://angular.dev/api/core/DestroyRef?ref=angularspace.com) on the `ngOnInit` to ensure that some action needs to be triggered on each child component as the component is unmounted from the UI. Note that on Angular 16+, `ngOnDestroy` [became optional as we are using now DestroyRef to indicate the component to do something once the component is destroyed](https://angular.dev/guide/components/lifecycle?ref=angularspace.com#destroyref). ```diff @Component({ ... }) - export class Component1 { + export class Component1 implements OnInit { + public destroyRef = inject(DestroyRef); public clicked = output(); + ngOnInit() { + this.destroyRef.onDestroy(() => { + console.log('Component 1 was destroyed'); + }); + } } @Component({ ... }) - export class Component2 { + export class Component2 implements OnInit { + public destroyRef = inject(DestroyRef); public clicked = output(); + ngOnInit() { + this.destroyRef.onDestroy(() => { + console.log('Component 2 was destroyed'); + }); + } } ``` DestroyRef will be executed when the component is destroyed, and what we will see, is that, once destroyed, we should see the respective message on the terminal. Now let's focus on the `AppComponent` to add the respective logic for our `clicked` event to become an observable ```diff import { ViewContainerRef, Component, ViewChild, provideExperimentalZonelessChangeDetection, output, inject, DestroyRef, OnInit, } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; + import { + outputToObservable, + takeUntilDestroyed, + } from '@angular/core/rxjs-interop'; ... @Component({ ... }) export class AppComponent { @ViewChild('targetSpace', { read: ViewContainerRef, }) private targetSpace!: ViewContainerRef; public injectC1() { const componentRef = this.targetSpace.createComponent(Component1); - componentRef.instance.clicked.subscribe(() => { + outputToObservable(componentRef.instance.clicked) + .pipe(takeUntilDestroyed(componentRef.instance.destroyRef)) + .subscribe(() => { + componentRef.destroy(); + }); } public injectC2() { const componentRef = this.targetSpace.createComponent(Component2); - componentRef.instance.clicked.subscribe(() => { + outputToObservable(componentRef.instance.clicked) + .pipe(takeUntilDestroyed(componentRef.instance.destroyRef)) + .subscribe(() => { + componentRef.destroy(); + }); } } ``` Once we have the [OutputToObservable](https://angular.dev/guide/components/output-fn?ref=angularspace.com#converting-an-output-to-an-observable) on place, we can `pipe` the response of the observable to use [TakeUntilDestroy](https://angular.dev/api/core/rxjs-interop/takeUntilDestroyed?ref=angularspace.com), which takes as a parameter the public destroyRef reference of the instantiated component. With this in mind, now we will see the respective `console.log` of each referenced component when destroyed: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/Arc_HP3rKC1vWl.gif) Using ****OutputAsObservable**, ****TakeUntilDestroy** and ****DestroyRef**, we can trigger events when the referenced component is destroyed ## Final Code This is the final resultant code. | main.ts | | ------- | ```js import { ViewContainerRef, Component, ViewChild, provideExperimentalZonelessChangeDetection, output, inject, DestroyRef, OnInit, } from '@angular/core'; import { outputToObservable, takeUntilDestroyed, } from '@angular/core/rxjs-interop'; import { bootstrapApplication } from '@angular/platform-browser'; @Component({ standalone: true, selector: 'app-component1', styleUrl: 'c1.css', template: `
This is Component 1
`, }) export class Component1 implements OnInit { public destroyRef = inject(DestroyRef); public clicked = output(); ngOnInit() { this.destroyRef.onDestroy(() => { console.log('Component 1 was destroyed'); }); } } @Component({ standalone: true, selector: 'app-component2', styleUrl: 'c2.css', template: `
This is Component 2
`, }) export class Component2 { public destroyRef = inject(DestroyRef); public clicked = output(); ngOnInit() { this.destroyRef.onDestroy(() => { console.log('Component 2 was destroyed'); }); } } @Component({ standalone: true, imports: [Component1, Component2], selector: 'app-root', styleUrl: 'main.css', template: `
this is static
`, }) export class AppComponent { @ViewChild('targetSpace', { read: ViewContainerRef, }) private targetSpace!: ViewContainerRef; public injectC1() { const componentRef = this.targetSpace.createComponent(Component1); outputToObservable(componentRef.instance.clicked) .pipe(takeUntilDestroyed(componentRef.instance.destroyRef)) .subscribe(() => { componentRef.destroy(); }); } public injectC2() { const componentRef = this.targetSpace.createComponent(Component2); outputToObservable(componentRef.instance.clicked) .pipe(takeUntilDestroyed(componentRef.instance.destroyRef)) .subscribe(() => { componentRef.destroy(); }); } } bootstrapApplication(AppComponent, { providers: [provideExperimentalZonelessChangeDetection()], }); ``` | main.css | | -------- | ```css .grid { margin-top:5px; display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:5px; } .container { border: 1px solid green; } ``` | c1.css | | ------ | ```css .c1{ background-color:#FF0000; color:#FFF; } ``` | c2.css | | ------ | ```css .c2{ background-color:#0000FF; color:#FFF; } ``` --- # Appendixes ## Working with Modules It is possible that you will encounter a project with a [module](https://angular.dev/guide/ngmodules?ref=angularspace.com). Previous versions of Angular, prior to version 16, used modules to group components and other Angular features to organize your code. However, this approach was not very effective from the perspective of developer experience (you spend more time grouping/organizing code than creating features to work right away). If you are using a [module](https://angular.dev/guide/ngmodules?ref=angularspace.com) in your project, the needed components for injection have to be declared in the **declarations** section: ```js @NgModule({ /** * Declare your components to be injected in the declarations section */ declarations: [ AppComponent, // your main component ComponentToInject1, // component to be used on programatic injection ComponentToInject2, // component to be used on programatic injection ], imports: [BrowserModule], providers: [CurrentDateService], bootstrap: [AppComponent], }) export class AppModule {} ``` Most of the code from the `standalone` approach will work here. Standalone components can't work along with Angular Modules, so you have to remove the definition for standalone in your component: ```diff @Component({ - standalone: true, selector:'app-component2', styleUrl:'c2.css', template: `
This is Component 1
` }) export class Component2 { public clicked = output() } ``` The only part that needs to be considered is the definition of the module and how it will load in the final code. This is how the final code looks like now with a module: | main.ts | | ------- | ```js import {platformBrowser} from '@angular/platform-browser'; import { AppModule } from './app.module'; platformBrowser() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ``` | app,module.ts | | ------------- | ```js import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import {Component1} from './c1.component'; import {Component2} from './c2.component'; @NgModule({ declarations: [ AppComponent, Component1, Component2, ], imports: [BrowserModule], providers: [], bootstrap: [AppComponent], }) export class AppModule {} ``` | app.component.ts | | ---------------- | ```js import {Component, ViewChild, ViewContainerRef} from "@angular/core"; import {Component1} from "./c1.component"; import {Component2} from "./c2.component"; @Component({ selector:'app-root', styleUrl:'main.css', template: `
this is static
` }) export class AppComponent { @ViewChild('targetSpace', { read: ViewContainerRef }) private targetSpace!:ViewContainerRef; public injectC1(){ const componentRef = this.targetSpace.createComponent(Component1) componentRef.instance.clicked .subscribe( () => { componentRef.destroy(); }) } public injectC2() { const componentRef = this.targetSpace.createComponent(Component2) componentRef.instance.clicked .subscribe( () => { componentRef.destroy(); }) } } ``` | c1.component.ts | | --------------- | ```js import {Component, output} from "@angular/core"; @Component({ selector:'app-component1', styleUrl :'c1.css', template: `
This is Component 1
` }) export class Component1 { public clicked = output() } ``` | c2.component.ts | | --------------- | ```js import {Component, output} from "@angular/core"; @Component({ selector:'app-component2', styleUrl :'c2.css', template: `
This is Component 2
` }) export class Component2 { public clicked = output() } ``` | main.css | | -------- | ```css .grid { margin-top:5px; display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:5px; } .container { border: 1px solid green; } ``` | c1.css | | ------ | ```css .c1{ background-color:#FF0000; color:#FFF; } ``` | c2.css | | ------ | ```css .c2{ background-color:#FF0000; color:#FFF; } ``` ## Summary Understanding how programatic injection works can lower down the architecture complexity of applications, making those quite maintainable and easy to operate. I hope you enjoyed this reading. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/Screenshot-2024-06-10-at-02.47.12.png) Note 1: I have added notes regarding component destruction events and how to handle this on the context of this exercise (June 10th, 2024). Shoutouts to [Jeff Getzin](https://www.jeffreygetzin.com/?ref=angularspace.com) who pointed out the need of documenting the handling of this kind of event. ### Discord Server is now OPEN URL: https://www.angularspace.com/discord-server-is-now-open/ Last updated: 2024-09-28T14:25:24.000Z Quick post - Discord server for the community is now open :). This was a community requested feature. It's up to you to make this place thrive, so discuss, ask questions, hang out! Invite link below: _This post is for subscribers only._ ### Using @Defer DeferViews in Angular 17 URL: https://www.angularspace.com/using-defer-deferviews-in-angular-17/ Last updated: 2024-06-06T15:29:09.000Z The Performance is one of the most important goals when we build web applications. In today's world, we work with a huge number of components and need to think about how to improve the bundle size in our apps. For example, this is great for online shops because it makes pages load faster and become usable more quickly, keeping customers happy. However, just breaking things into smaller pieces doesn't automatically make everything super fast. We still need to make sure our code and files are well-organized so everyone gets a smooth experience. The Angular team knows this and has recently launched some amazing features for lazily loading components, called "deferrable views" ### What are Deferrable Views? Deferrable views are the easiest way to implement lazy loading and split our code into chunks to improve user performance, loading the code only when we really need it. The deferrable view with `@defer` block allows us to load our components with less code and without imperative programming, making easy to perform lazy loading. The Angular team has provided some out-of-the-box triggers to lazily load components and there is a possibility to customize triggers to match with our business cases. But, wait a minute? We already have lazy loading in Angular 😠! We have a nice way to use lazy loading and create chunk-specific routing. For example, when the user navigates to a specific route, we then load a particular component. ```javascript import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', loadComponent: () => import('./pages/home/home.component').then((h) => h.HomeComponent), }, { path: 'products', loadComponent: () => import('./pages/products/products.component').then( (p) => p.ProductsComponent ), }, ]; ``` But when we want to load a component dynamic based on other events, we must write imperative logic & code, and combine with few things to make everything work. And it is not easy to handle complex business logic with lazily loading components. Today, we’re going to learn why should to use the `@defer` block in Angular to reduce the bundle size and to load dynamic component easy! ## Scenario Let’s say we create a landing page where we show a letter with best Angular 17 features and show **Kendo Ninja**. In the app, have the following components: - Letter: Shows a list of links of Angular's new features. - Ninja: Shows the Kendo UI Ninja. When the user ticks the checkbox, the `NinjaComponent` appears. By using the `@if` block, we change the boolean `accepted` to true, and it shows the `` component. The code looks something like this: ```html

Do you accept?

@if(accepted) { } ``` Let's clone the repo and install the dependencies to see the current code: ```bash git clone https://github.com/danywalls/learn-defer-views-angular.git cd learn-defer-views-angular npm i ``` After that, we can see special details in the output, a single `main.js` with `polyfills.js` and `styles.css`. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/84587605-0b1c-494b-9420-15a068011267.png) After that, run `ng serve -o` to see the application at [http://localhost:4200](http://localhost:4200/?ref=angularspace.com), and open the developer tools (F11) and go to the tab. We see a bunch of files, but one is important. The `main.js` contains the bundle of our app with all components in a single file. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/a3b155a8-dd59-4289-85fb-d7dd4b4df207.png) Maybe you're thinking why we are sending components that the user doesn't see or doesn't need to see only in specific cases ? For this exact reason, we must use a deferrable view to "manually" load our component lazily, loading a specific `.js` (or chunk). Let's do it! ## Manual Lazy Loading 😞 Open the `app.component.ts`, declare a new public variable `ninja` of type, then change the signature of the `accept` method to a promise. In the accept method, using dynamic import, set the variable jump with the dynamic import of `NinjaComponent`. ```javascript async accept(): Promise { const { NinjaComponent } = await import('./components/ninja.component'); this.ninja = NinjaComponent; } ``` The final code in `app.component.ts` looks like: ```javascript import {Component, Type} from '@angular/core'; import { RouterOutlet } from '@angular/router'; import {NgComponentOutlet} from "@angular/common"; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, NgComponentOutlet], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) export class AppComponent { public ninja!: Type; async accept(): Promise { const { NinjaComponent } = await import('./components/ninja.component'); this.ninja = NinjaComponent; } } ``` Next in the `app.component.html`, add the `ng-container` element with the `ngComponentOutlet` directive and set the value of jump. Remember to import the `NgComponentOutlet`, save the changes, and view the details in the output. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/f1d86af3-4f1f-4966-892d-6d95cefb32de.png) We got a new chunk, `chunk-XQI5NWYI.js` for the `NinjaComponent` and it is perfect! Save changes and reload the page. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/1c425c02-4819-480a-8b7a-ac2fd4d921e7.gif) When we tick the checkbox input, the desired javascript chunk is downloaded and the component loaded! This is highly optimized to only download the chunk when the user clicks the checkbox. But do you think this can scale in the future? What happens if tomorrow we want to add new cases to load the component like: - Load when the users scroll. - When the browser isn't busy. - Show a loading indicator while loading. At this moment, it's not easy. This is when we must switch to `defer`. ## The Defer Since Angular 17 we have the `@defer` block available as part of the new Angular Control Flow, allowing us to write declarative code and to lazily load our components easy. That is loading the javascript bundle and rendering the component when some condition or trigger matches and the dependencies are ready. The `@defer` block can work combined with `@error`, `@placeholder`, `@loading` and custom triggers. Before we continue, let's refactor our code. - Add the `NinjaComponent` to the imports section. - Declare a new variable `accepted` initialized as `false`. - Update the accept method to `void`, and inside, set the accepted value. The final code looks like: ```javascript import {Component} from '@angular/core'; import { RouterOutlet } from '@angular/router'; import {NgComponentOutlet} from "@angular/common"; import {NinjaComponent} from "./components/ninja.component"; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, NgComponentOutlet, NinjaComponent], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) export class AppComponent { accepted = false; public accept(): void { this.accepted = !this.accepted; } } ``` > The `@defer` block works only with standalone components. In the HTML markup, update to use the `@defer` block; it replaces the area with a component when the browser state is idle by default, but we also combine it with triggers `when` and `on`. For example, the `defer` block will render `when` the `accepted` variable is true. ```html @defer (when accepted) { } ``` Save changes, the output looks the same without all the boilerplate with the dynamic `import` and the `imports` and `NgComponentOutlet` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/fd8665d4-e1ba-4635-a142-6cb833adad06.png) Everything continues working with lazy loading and the ninja component is loaded eagerly only when the user clicks as always. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/a4f22c95-e63b-4551-9027-954af45544a3.gif) but of course we also have the `on` trigger to trigger the block based to a list of trigger: #### on viewport The deferred block is triggered when the element enters the viewport area, using the [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection%5FObserver%5FAPI?ref=angularspace.com) API. ```javascript @defer (on viewport) { } @placeholder {
} ``` #### on interaction The deferred block is triggered when the mouse has hover over the trigger area. ```javascript @defer (on interaction) { } @placeholder {
} ``` #### timer The deferred block is triggered after a specified time. ```javascript @defer (on timer(4s)) { } @placeholder {
} ``` We can continue show a nice examples of trigger but I recommend, read more about [triggers](https://angular.dev/guide/defer?ref=angularspace.com#defer) and move foward to use @placeholder 😊 and @loading blocks. ### The @placeholder and @loading We are going to lazy load the `Ninja` component when the user clicks to show it, but with a special process, we want to show a placeholder in the area where the Ninja appears. It helps to us, avoid page shift saving this space for the Ninja component. let's show the text "Are you ready" in the area where the `NinjaComponent` will appear. ```html @defer (when accepted) { } @placeholder {

Are you ready ?

} ``` Save and reload the page, and you will see the message "Are you ready?" in the area where the NinjaComponent will appear. #### how @placeholder and @loading work together? Well, the `@placeholder` is what you see first. It's like a temporary image or message that shows up while the rest of your content is getting ready. The `@loading` appears as soon as things start to load. It might be a spinning icon or a progress bar that lets you know something is happening. Once your main content is ready, both `@placeholder` and `@loading` go away. > Important: The `@loading` block automatically takes the place of the `@placeholder` when loading starts. Both of these blocks are helpful tools for keeping your users entertained while they wait for the real content to appear. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/2a1da0a4-05a1-4e7d-b67f-bbff1c77d304.gif) Great, it works! Now let's enhance the loading process. For instance, I want to display the placeholder, but also show a message while the component is loading. To do this, I'll combine the loading block with the `minimum` parameter, setting it to 5 seconds. > The `minimum` setting is used to make sure the loading icon or message shows up for at least a little while, even if your content loads really fast. ```html @loading (minimum 5s) {

The Ninja is coming...

} ``` ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/d3439442-43c0-44f7-be09-1e56768b0487.gif) Save changes and play again! Everything works as expected. Remember, the placeholder and loading are optional blocks, but they are very helpful in improving the user experience. > Important: The code inside the @placeholder and @loading are bundled in the main.ts file. ## Conclusion We explored the advantages of using Angular 17's @defer feature to dynamically lazy-load components, which boosts performance by loading components only when they are needed. The `@defer` block simplifies the process by removing the need for manual, imperative coding. We also experimented with `@defer` block using various triggers such as viewport entry, user interactions, and timers. Additionally, we enhanced user experience by using `@placeholder` and `@loading` blocks to effectively manage UI elements during load times. - Starting point: [https://github.com/danywalls/learn-defer-views-angular](https://github.com/danywalls/learn-defer-views-angular?ref=angularspace.com) - Dynamic Imports [https://github.com/danywalls/learn-defer-views-angular/tree/dynamic-imports](https://github.com/danywalls/learn-defer-views-angular/tree/dynamic-imports?ref=angularspace.com) - Defer views [https://github.com/danywalls/learn-defer-views-angular/tree/feature/defer-views](https://github.com/danywalls/learn-defer-views-angular/tree/feature/defer-views?ref=angularspace.com) --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/Screenshot-2024-06-04-at-03.42.13.png) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/06/Ji9I6k1l.jpg)](https://www.simplified.courses/e-store?ref=angularspace.com) ### Angular Space Job Offer #1 (Senior/Lead Angular Engineer) URL: https://www.angularspace.com/angular-space-job-offer-1-senior-lead-angular-engineer/ Last updated: 2024-05-31T14:54:32.000Z I'm proud to announce first Job Offer via Angular Space! Many more to come. These job offers are screened by me and are done via special collaboration with various companies & recruiters. Submissions are open for 3 days from publication. Job offer details: _This post is for subscribers only._ ### Frontend Nation x Angular Space Partnership URL: https://www.angularspace.com/frontend-nation-x-angular-space-partnership/ Last updated: 2024-05-28T00:39:55.000Z Hi there, **This event is 100% FREE!** If you are looking for a place to gather insights into all major frontend technologies in 2024, we have a can’t-miss event just for you and we’re sponsoring! **Frontend Nation 2024**, an online event brought to you by the organizers of Vue.js Nation, Nuxt Nation, and Vue.js Forge, is happening this June! 🔥 [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/3emDNzE9-1.gif)](https://go.frontendnation.com/aspacemail?ref=angularspace.com) **Here’s why we’re a proud sponsor of Frontend Nation 2024:** ✨ **A Stellar Lineup of Speakers Across the Frontend Ecosystem!** Hear from a distinguished lineup of over 50+ speakers, including industry leaders from the Vue.js universe like Evan You (Creator of Vue.js and Vite) and Alex Kyriakidis (Founder & CEO of Vue School), as well as experts from other frameworks such as React, Angular, Svelte, and more. Featured speakers include Minko Gechev, Angie Jones, Kent C. Dodds, Anthony Fu, Sébastien Chopin, Kelly Vaughn, Debbie O’Brien, and many more experts whose talks you don’t want to miss. ✨ **Technology Roadmaps and Development Insights Revealed** Catch up with the latest advancements in Vue.js, React, Angular, Vite, Next.js, Nuxt, TypeScript, and more. Get comprehensive insights into the features, benefits, roadmaps, exciting projects, and plans for the upcoming year on key technologies. Apart from the insights in the latest tech enhancements, you will have a chance to enjoy talks on product and leadership. [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/ImJe1Kt0.png)](https://go.frontendnation.com/aspacemail?ref=angularspace.com) ✨ Exclusive Industry Insights Explore diverse topics including Frontend + 3D, Accessibility, Security, Testing, Performance, and the latest JavaScript developments. The event also features expert panels on the subjects of the Future of Frontend, and Vite 6! ✨ **Attendance is 100% FREE!** Frontend Nation is committed to being the largest and only 100% free online conference covering the comprehensive world of frontend technologies. **📆** [**→ Join Frontend Nation for free**](https://go.frontendnation.com/aspacemail?ref=angularspace.com) June 4-7, 2024This event only happens once a year, so don’t miss out! ### Introduction to PrimeNG: A Rich UI Component Library for Angular URL: https://www.angularspace.com/introduction-to-primeng-a-rich-ui-component-library-for-angular/ Last updated: 2024-05-26T23:51:42.000Z As an Angular developer, you're probably aware of the numerous UI libraries available, such as Angular Material, PrimeNG, Clarity, NG Bootstrap, Kendo UI, Taiga UI, daisyUI and Spartan. While these libraries have their strengths, in this blog post, we'll explore why PrimeNG stands out as a rich and versatile choice for your Angular projects. ## What is PrimeNG? [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/dn/exploring-primeng/dalenguyen/exploring-primeng/exploring-primeng.md?ref=angularspace.com#what-is-primeng) PrimeNG is an extensive collection of rich UI components for Angular. It’s developed by PrimeTek Informatics, a company with years of expertise in developing open-source UI component libraries. PrimeNG is also a sibling of the popular popular UI library such as JavaServer Faces, PrimeReact and PrimeVue. One of the key advantages of using a UI library such as PrimeNG is the speed it brings to your development process. It allows you to quickly prototype and build application without having to create complex UI components from scratch. Now, let's start the process of setting up PrimeNG in your Angular project. ## Setting up your Angular project with PrimeNG [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/dn/exploring-primeng/dalenguyen/exploring-primeng/exploring-primeng.md?ref=angularspace.com#setting-up-your-angular-project-with-primeng) In this part, we will explore how to add PrimeNG and its dependencies to your Angular project, import the necessary styles, and utilize PrimeNG components in your templates. ### Installing PrimeNG [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/dn/exploring-primeng/dalenguyen/exploring-primeng/exploring-primeng.md?ref=angularspace.com#installing-primeng) Before starting to integrate PrimeNG into your Angular project, make sure that you set up a fresh project to begin with. If not, you can create a new project by running the following command: ``` npx @angular/cli new ``` You can also add PrimeNG to your existing project. Please check the version comparability at [https://primeng.org/lts](https://primeng.org/lts?ref=angularspace.com). Basically, PrimeNG release cycle does match with Angular 6 months release cycle. If you are using Angular v15, you should install PrimeNG v15 to avoid breaking changes. Now follow these steps to install PrimeNG in your Angular project: 1. Open a terminal or command prompt in your project directory. 2. Run the following command to install PrimeNG and save it as a dependency in your project: ``` npm install primeng ``` After the installation process, we will `primeng` in the package.json file in your root directory. The `primeng` package is all that you need to get started with integration. In the next section, we will add some styles and themes to your application. > **Note**: In this post, we'll utilize Angular & PrimeNG version 17 for demo purpose. ### Importing PrimeNG styles into your Angular application [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/dn/exploring-primeng/dalenguyen/exploring-primeng/exploring-primeng.md?ref=angularspace.com#importing-primeng-styles-into-your-angular-application) The Theme and Core styles are essential CSS files for the components. You can find the comprehensive selection of available themes in the Theming section at [https://primeng.org/theming#themes](https://primeng.org/theming?ref=angularspace.com#themes). To incorporate the styles, you can import them either in the `angular.json` or `src/styles.css` file. In this section, we will utilize the `lara-light-blue` theme as a demonstration. ```css // styles.scss /* You can add global styles to this file, and also import other style files */ @import 'primeng/resources/themes/lara-light-blue/theme.css'; @import 'primeng/resources/primeng.css'; ``` In addition, each theme has its own font family, it's suggested to apply it to your application to achieve a unified look. ```css // styles.scss body { font-family: var(--font-family); } ``` ### Using PrimeNG components in your templates [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/dn/exploring-primeng/dalenguyen/exploring-primeng/exploring-primeng.md?ref=angularspace.com#using-primeng-components-in-your-templates) The section focuses on incorporating PrimeNG components seamlessly into your templates. We will learn how to leverage the extensive library of PrimeNG components effectively within your web applications. Follow these steps to utilize PrimeNG components: - Open a component template file (.html) where you want to use PrimeNG components. - Place the PrimeNG component selector in the template where you want the component to appear. For example, to add a button component, use the following code: ```html ``` For the standalone component, you will import the PrimeNG component into the Angular component that you are working on: Call to Action block example ```js import { Component } from '@angular/core'; import { ButtonModule } from 'primeng/button'; @Component({ standalone: true, imports: [ButtonModule], selector: 'app-root', template: ` `, }) export class AppComponent {} ``` In the previous code snippet, we have `AppComponent`, which utilizes `ButtonModule` from PrimeNG. `p-button` is an indicator that it's a PrimeNG button component. As a result, you will have a styled button on your browser: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/button.png) **PrimeNG button component* ## Increase development speed with PrimeBlocks [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/dn/exploring-primeng/dalenguyen/exploring-primeng/exploring-primeng.md?ref=angularspace.com#increase-development-speed-with-primeblocks) [PrimeBlocks](https://blocks.primeng.org/?ref=angularspace.com) is a collection of prebuilt UI blocks crafted with PrimeFlex developed by PrimeNG. These blocks are designed to simplify the development process by providing ready-to-use UI elements that are commonly used in web applications. PrimeBlocks offers a variety of UI blocks, including Navbar, Breadcrumbs, Tabs, Footer, Notification, Dialog and more. These UI blocks are highly customizable and can be easily integrated into your Angular projects. Before getting started, make sure that you have PrimeFlex and PrimeIcons installed by running the following command: ``` npm install primeicons primeflex ``` After that you can add the styling to `styles.scss` file: ```css // styles.scss @import "primeicons/primeicons.css"; @import 'primeflex/primeflex.scss'; ``` > **Note**: PrimeFlex serves as a lightweight, responsive CSS utility library, similar to TailwindCSS. It’s helpful to conceptualize PrimeFlex as the equivalent of TailwindCSS, while PrimeBlocks can be compared to TailwindUI. After the preparation is done, you can implement any blocks from PrimeBlocks by selecting the `Code` option from the block: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/call-to-action-source.png) **Call to Action block* After copying the code, you can add it to your Angular template. Here is an example code of Call to Action block: ```html
 POWERED BY DISCORD
Join Our Design Community
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velit numquam eligendi quos.
``` As a result, you will have a nice Call to Action UI block that's ready to use on your web site: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/call-to-action.png) **Call to Action block example* > **Note**: PrimeNG also provide a plenty of free of paid templates that you can apply to create your web applications with a little of modification to suite your business needs such as [Sakai](https://www.amazon.com/dp/1803249811?ref=angularspace.com) (Admin dashboard template). ## Summary [](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/dn/exploring-primeng/dalenguyen/exploring-primeng/exploring-primeng.md?ref=angularspace.com#summary) I hope that you enjoyed the introduction to the world of fast development with PrimeNG. If you would like learn more about how to work with PrimeNG and Angular, checkout my [Next-Level UI Development with PrimeNG](https://www.amazon.com/dp/1803249811?ref=angularspace.com) book or visit the main [PrimeNG website](https://primeng.org/?ref=angularspace.com). --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/Screenshot-2024-05-23-at-04.50.54.png) --- ### Decomposition: your real superpower URL: https://www.angularspace.com/decomposition-your-real-superpower/ Last updated: 2024-05-16T01:50:58.000Z You can be learning Angular for quite some time, improving your skills with various aspects of the framework, learning patterns and best practices, but at the end, when you master your craft – there is one skill you can keep improving forever. It’s the ability to properly decompose complex tasks into digestible pieces. This is one of the most important traits of a good architect. If you feel like you have confidence in your senior level knowledge, I advise you focus your efforts in this direction to keep growing. Let’s pick a difficult task and learn how we can approach it in a maintainable, future proof way with good architecture. I’ve been working on a component library [Taiga UI](https://github.com/taiga-family/taiga-ui?ref=angularspace.com) for many years now, having my fair share of mistakes, learning opportunities, insights and lessons learned. I believe a good example to explore the outlined subject is popovers. We will focus mostly on dropdowns, but this exact approach is also implemented in [Taiga UI](https://taiga-ui.dev/?ref=angularspace.com) for hints. > ***"Give me six hours to chop down a tree and I will spend the first four sharpening the axe."*** \- Abraham Lincoln ## Understanding the task While we all patiently wait for the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover%5FAPI?ref=angularspace.com) to ship, or rather for browsers we have to support to catch up, we have to take care of many things ourselves. We will use “portals pattern” which is a common way to show popover content as it saves us from overflow/scrollbar and z-index issues. You can read more about it in my [older article](https://dev.to/taiga-ui/demystifying-taiga-ui-root-component-portals-pattern-in-angular-1fal?ref=angularspace.com). So, with that prerequisite in mind let’s imagine we are tasked with creating a flexible dropdown infrastructure for our Angular app/library. First, we must breakdown the job into high level problems we need to solve: 1. We need to know **what** to show 2. We need to know **where** to show it 3. We need to know **when** to show it Now that we have those questions written down, we can tackle them one by one. Note that they are pretty independent, which is a good sign. Decomposing is exactly about figuring out which parts of the solution can work in isolation from one another, making each responsibility small and maintainable. Let’s go over those list items and explore how to address them and why it has to be flexible in the first place. ## What to show? In Angular we can pass pieces of content as interpolation, templates or dynamic components. This section is not about that. Dropdowns have a particular design around the content they show. So, we will focus on that. For passing the content I will direct you to my [article about a library](https://medium.com/angular-in-depth/agnostic-components-in-angular-2427923b742d?sk=7da9f7e1da12200bdeaa85ac493b9e1b&ref=angularspace.com) I created called **Polymorpheus**. Basically, it is a universal outlet, allowing you to use all those types of content I mentioned above without worrying about `ngTemplateOutlet`, `ngComponentOutlet` or interpolation – it is all taken care of for you by a polymorphic structural directive. Why do we need to be able to show different components as our content containers? Sometimes our dropdowns or hints look radically different depending on the context, but that doesn’t mean we need to come up with completely new infrastructure for each of those cases. Below you can see GIFs of different popovers: ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/dropdown-1-ezgif.com-crop-1.gif) Desktop dropdown ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/dropdown-2.gif) Mobile dropdown We can achieve this by dependency injection. At the very end, our popovers are going to be dynamically created components. So why don’t we provide the component that needs to be created with a DI token? This way we can even have a default component and our directives can provide different implementations, as in the case for [LineClamp](https://taiga-ui.dev/components/line-clamp?ref=angularspace.com) hint component that uses CSS `line-clamp` to truncate content to a given number of lines and shows whole text in a popover upon touch/pointer hover, as opposed to default hint bubble with arrow. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/hint-1.gif) Default hint ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/hint-2.gif) LineClamp hiint This was the most straightforward part. Now that we know what to show we need to figure out where to show it. ## Where to show? Determining position is not a trivial task. As a matter of fact, it’s probably going to be the most code you will write for this whole feature. We will not delve into actual JavaScript here, it’s pretty much just some arithmetics, based on the things we will discuss in this article. First, each popover typically has some sort of a host. It can be a hint icon, a dropdown button or even selection for context menus or pointer position for hints following the cursor in a pie chart. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/hint-3.gif) Hint following the pointer ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/dropdown-3.gif) Context menu inside a textarea In order to determine position, we will need to know where our host is located and how it looks. For that we will create our first abstract entity – `RectAccessor`. All it does is provide us a method to request `DOMRect` of our host when we need it. Next, depending on the implementation, we need a second abstract class – `PositionAccessor`. It will allow us to pass popover `DOMRect` and retrieve the coordinates for our popover to be shown at, based on popover size and host `RectAccessor`. For example, **LineClamp** popover is shown at the exact position of the host, while hints are positioned so that the arrow head is pointing in the middle of the host and dropdowns are shown above/below the host at some given distance. These abstract entities are what our components are going to work with and different directives will provide different implementations. ## When to show? Now the last thing we need to take care of is showing and hiding our popovers. We can have different triggers, for example we can show hints on pointer hover or keyboard focus, we can show dropdowns upon clicks or context menus on right clicks, we can have manual popovers controlled by something else. In order to achieve this, we introduce another term – a `Driver`. Basically, it’s just an `Observable` to toggle visibility of our thing (which we can call in contrast a `Vehicle`). Once again, directives will provide implementation. It’s a good idea to use it as a *multi token* so that you can augment already existing drivers with new ones, for example if your dropdowns typically open on click but a particular one you want to also show on hover. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/dropdown-4.gif) Multi level context menu It is a textbook use case for **RxJS** since in a nutshell, it’s event management. You can chain multiple `fromEvent` calls into one resulting stream. For example, you might want to open a dropdown upon clicking, arrow down keypress or `pointerenter`, close it on the Escape button or click outside the host or the dropdown itself. While at the time of writing Angular shifts away from **RxJS** being a required knowledge I still strongly suggest you invest some time into getting proficient with it. Not only because it is super powerful when used properly, but because chances are, it’s [coming to native browser JavaScript](https://github.com/WICG/observable?ref=angularspace.com). You can check out my little [repository](https://github.com/AngularWave/rxjs-challenge?ref=angularspace.com) of bite size **RxJS** challenges to hone your skills. ## A recap We are implementing a popover infrastructure. We decided to go with the portal approach and create dynamic components as portals above our app content. We have an `InjectionToken` holding a component we will create, so that directives can override it with different implementations. Depending on our needs we have directives that help us determine position of the popover providing us host `DOMRect` and an algorithm to determine, based on it and popover size, where to put the popover component. And to control when to show/hide our popover we have another set of directives. We can have default implementations in the basic hint/dropdown directive and fallback to it, unless custom behavior was provided by some directive. Let’s explore a few examples from [Taiga UI](https://taiga-ui.dev/?ref=angularspace.com): ```HTML ``` ```HTML
In this block hint follows cursor
``` Because of hierarchical nature of DI we do not need access to the actual dropdown directive to provide a custom component somewhere up the tree: ```HTML Select user ``` ## Actual code Here’s a stripped-down version of the code described above for a typical dropdown. Obviously real life situations require more nuances taken care of, but this will give you the general outlook. First we need a component to show. As we discussed, it will be a token with default value: ```JS export const DROPDOWN_COMPONENT = new InjectionToken('', { factory: () => DropdownComponent, }); ``` In the GIF above we saw a mobile sheet-like dropdown for Select. This can be achieved by a directive: ```JS @Directive({ // ... providers: [ { provide: DROPDOWN_COMPONENT, useFactory: () => isMobile(inject(DOCUMENT).defaultView.navigator.userAgent) ? DropdownMobileComponent : inject(DROPDOWN_COMPONENT, {skipSelf: true}), }, ], }) export class DropdownMobileDirective {} ``` This directive injects `DOCUMENT` to test `userAgent`, and if we are on a mobile device — provides mobile implementation. Otherwise it falls back to the previous value up the DI hierarchy tree. Next we will take care of positioning. Like I said, we’ll not explore the position calculation so imagine we have `PositionDirective` to do all the math with coordinates. But to do so we will need a `RectAccessor`: ```JS export class RectAccessor { private readonly element = inject(ElementRef).nativeElement; // Required RectAccessor method public getRect(): DOMRect { return this.element.getBoundingClientRect(); } } ``` We will need a driver directive, connecting all the drivers with the vehicle: ```JS export class DriverDirective { private readonly vehicle = inject(Vehicle); // Injecting multi token Driver and merging all the streams private readonly sub = merge(...inject(Driver)) .pipe(distinctUntilChanged(), takeUntilDestroyed()) .subscribe(this.vehicle.toggle.bind(this.vehicle)); } ``` A dropdown directive is the vehicle: ```JS @Directive({ // ... providers: [{ provide: Vehicle, useExisting: DropdownDirective }], hostDirectives: [DriverDirective, RectAccessor, PositionDirective], }) export class DropdownDirective { // To handle component creation private readonly service = inject(DropdownService); private readonly component = inject(DROPDOWN_COMPONENT); public dropdown = input(''); // string, template, component // Required Vehicle method public toggle(show: boolean): void { this.dropdownBoxRef = this.service.toggle(this.component, show); } } ``` Our `DROPDOWN_COMPONENT` will inject `PositionDirective` once created to query it for the position it should be placed at. And `DropdownService` is just responsible for creating and destroying dynamic components as portals somewhere in the DOM where we want to place them. Now all we need is a driver to toggle visibility. The most basic one is manual dropdown, controlled by external input: ```JS @Directive({ // ... providers: [{ provide: Driver, useExisting: DropdownManual, multi: true }], }) export class DropdownManual extends Observable { public open = input(false); constructor() { super(subscriber => toObservable(this.open).subscribe(subscriber)); } } ``` More complex drivers such as hover or context menu, keyboard or clicks follow the same basic logic — we compose a stream and provide it as a `Driver`. Exploring the code to do so goes beyond the scope of this article. ## Conclusion Angular is a very well-designed framework. It promotes architectural best practices with directives composition, dependency injection for altering implementations, multi tokens, services and `hostDirectives` – all the tools you might need to create maintainable, extendable solutions. When customers come up with new requests, I’m confident it would not take a complete feature rewrite because, when properly implemented, Angular code does not just look beautiful – it also allows you to expand capabilities keeping fundamentals intact. So, my biggest advice to you here is: **before writing any code, think hard whether or not this code needs to be written**. Take some time to properly architect your feature, think beyond your current specifications, try to build infrastructure for accommodating needs, rather than a concrete solution for specific requirements. You have everything you need, learn how to use it well and not only will you be a brilliant addition to any team, but writing code will be a pleasure. After all, we went into engineering because we love solving problems and Angular is effective and ergonomic in that regard. --- ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/Screenshot-2024-05-16-at-03.42.55.png) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/yx3RkJdA--1-.jpg)](https://angular-university.io/?ref=angularspace.com) ### Angular Space Mentors Part #1 URL: https://www.angularspace.com/angular-space-mentors-part-1/ Last updated: 2024-05-04T11:42:20.000Z Please give a warm welcome to Angular Space Mentors! ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/05/IMG_9567-1.jpeg) This is a first set of Mentors that believed in my vision and decided to help craft this wonderful endeavor. Mentors are going to review and validate every single article published on Angular Space. They are also a key part of Angular Space Writer Mentorship Program announced yesterday. I’m going to announce more experts soon! ### Apply to join Angular Space Mentorship Program URL: https://www.angularspace.com/angular-space-mentorship-program/ Last updated: 2024-05-31T14:23:10.000Z More about the program here: To apply please become a Members as this is member only perk :) _This post is for subscribers only._ ### Prevent Angular Provider Misuse URL: https://www.angularspace.com/prevent-angular-provider-misuse/ Last updated: 2024-05-16T02:22:37.000Z Since many Angular applications are standalone these days, we are all used to using different provider functions. The most common ones nearly everyone has seen are **provideHttpClient()** and **provideRouter()**. These functions register providers under the hood so that you can use the provided functionalities. If you look at the naming, you could think these are to use in your Components **providers: \[\]** array, but in fact, they were created to be used in [environment injectors](https://angular.io/api/core/ENVIRONMENT%5FINITIALIZER?ref=angularspace.com). ### What's the problem? If you start to write your own logger library, you come to a point where you add a **provideLogger()** function that registers a provider for the configuration. ```js export function provideLogger(config: Partial) { return { provide: LoggerConfig, useValue: config, }; } ``` You can use the function in a component or a directive injector without an error. In the long run, this can lead to errors or unexpected behavior in your library. If you design your **provideLogger** function to configure the logging behavior globally, using it in components with different parameters can cause the logger to behave inconsistently, even though this is not meant to be possible. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/04/plain-providers-array.png) The provideLogger function used within the AppComponent ### The solution Angular provides the **makeEnvironmentProviders** function, which returns an **EnvironmentProviders** type instead of a **provider** array. This type wraps the providers to ensure your function is only used in the right place, your **bootstrapApplication** / **ApplicationConfig**. You need the **provideLogger** implementation to use **makeEnvironmentProviders**: ```js import { makeEnvironmentProviders } from "@angular/core"; export function provideLogger(config: Partial) { return makeEnvironmentProviders([ { provide: LoggerConfig, useValue: config, }, ]); } ``` Now, if you try to use **provideLogger()** in a component (or directive), you'll see an error. This error tells you faster that the providers are designed to be used with environment providers and can't be used here. ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/04/with-make-environment-providers.png) provideLogger with makeEnvironmentProviders [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/04/Screenshot-2024-04-23-at-18.33.54-2.png)](https://twitter.com/GeromeDEV?ref=angularspace.com) --- [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/04/yx3RkJdA.jpg)](https://www.angular-university.io/?ref=angularspace.com) ### Angular Signals: Best Practices URL: https://www.angularspace.com/signals-best-practices/ Last updated: 2024-05-16T02:20:34.000Z In this article, I share my experience of working with Angular Signals after almost a year of using them. ### When to use Signals? 1. In templates; 2. When you need to react to changes in a value without a time aspect. In Angular templates, Signals are better than Observables: they schedule Change Detection without any pipes, they are glitch-free, and you can read the same signal multiple times and it will be “free” in terms of performance (and read values are guaranteed to be the same). There are other reasons that are not so easy to explain briefly, but that’s already enough to make a rule: every variable (that might change) in your **new** templates should be a Signal. Outside of templates, Signals also can be used for reactivity, but, as I mentioned, without a time aspect. I once wrote a post on Twitter about it, and now I’ll post it here, updated and improved: There are two ways to create reactive variables in Angular: Observables and Signals. If you describe in words, how your variable should express its reactivity, you’ll see what you need to use. If the role of a variable can be described as conditions, then you need a Signal: - “if this variable has this value, then display this list” - “if this variable has this value, this button is disabled” If the description of a variable’s role includes words, related to time, you need an Observable: - “**when** the cursor moves…” - “**wait** for the file uploading event and then…” - “every **time** this event happens, do this…” - “**until** this event…” - “for N **seconds** ignore…” - “**after** this request…” Signals have no time axis, and they can not delay a value — they always have a value, and their consumers should be always able to read it. Consumers of Signals, `computed()`, `effect()`, and templates do not guarantee that they will read every new value written to the Signals they watch. An updated Signal will be *eventually* read, not instantly after the update as it happens with Observables. Consumers decide when they will read the new value using their scheduling mechanisms. It might be “in the next task,” “during the next Change Detection cycle,” or at some other moment, up to the consumer. ### When to use `computed()`? Whenever you like! `computed()` is the best thing in Angular Signals, incredibly handy and safe to use. Using `computed()`, you’ll make your code more declarative (you can read more about it in [this article](https://medium.com/@eugeniyoz/creating-angular-components-template-first-declarative-approach-00c4a4791270?ref=angularspace.com)). There are just two rules about the usage of `computed()`: 1. Do not modify things in `computed()`. It should compute a new result, that’s it. Do not modify the DOM, do not mutate variables using `this`, and do not call functions that might do that. Do not push values to Observables — it will cause unintentional reactive context propagation (explained below for `effect()`). `computed()` should not have side effects, it should be a [pure function](https://en.wikipedia.org/wiki/Pure%5Ffunction?ref=angularspace.com). 2. Do not make asynchronous calls in `computed()`. This function does not allow modification of Signals (and it is amazingly helpful), but it can not track asynchronous code. Moreover, Angular Signals are strictly synchronous, so if you want to use asynchronous code in `computed()`, you are doing something wrong. So, no `setTimeout()`, no Promises, no other asynchronous things. ### When to use `effect()`? Angular docs say that you’ll rarely need `effect()` and [discourage](https://angular.dev/guide/signals?ref=angularspace.com#use-cases-for-effects) you from using it ([copy](https://gist.github.com/e-oz/99d04094abe5007d882f35879eb00da3?ref=angularspace.com), if docs will be edited). And that info is correct: you rarely need `effect()`… if your code is declarative ;) The more imperative your code, the more often you’ll need `effect()`. There is no code without imperative parts, but we all should try to make our code as declarative as possible, so we do need to use `effect()` as rarely as possible. Besides the dangers, mentioned by Angular docs (infinite loops, change detection errors), there is another thing, that might be quite nasty: effects are executed in a reactive context, and any code you call in effect, will be executed in a reactive context. If that code reads some signals, they will be added as dependencies to your effect. [Here](https://github.com/angular/angular/pull/54614?ref=angularspace.com) Alex Rickabaugh explains the details. I still don’t want to encourage you to use `effect()`, but I’ll give you advice on how to use it as safely as possible: 1. The function you provide to `effect()` should be as small as possible. This way it will be easier to read and spot erroneous behavior. 2. Read signals first, then wrap the rest of the effect into `untracked()`: ```js effect(() => { // reading the signals we need const a = this.a(); const b = this.b(); untracked(() => { // rest of the code is here - this code should not // modify the signals we read above! if (a > b) { document.title = 'Ok'; } }); }); ``` ### Mixing Signals and Observables …is ok! Your code will have Signals and Observables, at least because Signals can not be used for every kind of reactivity (see detailed explanation above). It is not an issue, it is perfectly fine. If you need some value from an Observable in your `computed()`, create a Signal using `toSignal()` (outside of `computed()`). If you need to read a Signal in Observable’s `pipe()`, there are two ways: 1. If you need to react to changes in that Signal in your Observable, then you need to convert that Signal into Observable and add it using some join operator. 2. If you are sure you just need the current value of a Signal and you don’t need to react to its changes — you can read a Signal directly in your operators or `subscribe()`. Observable is not a reactive context, so you don’t need `untracked()` here. [![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/04/Screenshot-2024-04-23-at-18.33.54-1.png)](https://twitter.com/GeromeDEV?ref=angularspace.com) Reviewers ### Always unsubscribe. No exceptions. Debate closed. URL: https://www.angularspace.com/always-unsubscribe-no-exceptions-debate-closed/ Last updated: 2024-12-05T21:57:14.000Z --- So many years with Angular. So many years with RxJS. Yet people still fight over “when NOT to unsubscribe”. ### Truth is. It’s not even worth the conversation. Just unsubscribe always. Simple. I have recently posted the same advice on my social media(X/LinkedIn). ![](https://cdn-images-1.medium.com/max/1200/1*657PZt8vzzgyqvgoR5cHQA.png) ![](https://cdn-images-1.medium.com/max/1600/1*RN0biXQbqy72j3sEUzlekw.png) While majority agreed without a doubt. About 10% decided that it’s a perfect opportunity to showcase deep and expert knowledge of how there is NO NEED to always unsubscribe. The prime and crown example: - HttpClient cold observable I have been told that: - It’s a terrible advice - It’s a big overhead to always unsubscribe - People should never follow this advice - It’s absolutely wrong - It’s redundant code - It’s a bold assumption - Go ask Angular Team and so on… Yes technically HttpClient completes automatically in like 99% of cases. However this doesn’t mean that you have to deliberately find and safe guard places where you subscribe to HttpClient Observable to avoid unsubscribing. ### Simple “Why”: - It doesn’t make any sense - There is 0 overhead to unsubscribe since you have to do it in other places as well ( should be in your blood already cmon!” - Value consistency over “being smart about things” - Never think about when not to unsubscribe and focus on important things. - Value consistency… - Value consistency… ### Not convinced? Advanced “Why”: - Citing a friend, because i could have not put it together in a batter way: ![](https://cdn-images-1.medium.com/max/1600/1*84IP9wxxtzGwLmI2K9Oscg.png) - Additionally this conversation has been “silently” resolved in one of the GitHub issues from 2022\. Feedback from “Engineering” means response from devs. ![](https://cdn-images-1.medium.com/max/1600/1*APPXvEFWw6KW6Zm3WAPSjQ.png) source: [https://github.com/angular/angular/issues/46542](https://github.com/angular/angular/issues/46542?ref=angularspace.com) - And finally in new Angular docs website. It’s clearly stated that: > *Once the response returns, Observables from HttpClient usually complete (although interceptors can influence this).* > *Because of the automatic completion, there is usually no risk of memory leaks if HttpClient subscriptions are not cleaned up. *However, as with any async operation, we strongly recommend that you clean up subscriptions when the component using them is destroyed*, as the subscription callback may otherwise run and encounter errors when it attempts to interact with the destroyed component.* source: [https://angular.dev/guide/http/making-requests#http-observables](https://angular.dev/guide/http/making-requests?ref=angularspace.com#http-observables) Now you can use this resource if your teammates keep nitpicking that you are unsubscribing from HttpClient Observables. Or any other they think it’s “safe” not to do it. ### If you are still not convinced, hear it from Angular Team Alex Rickabaugh ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/12/Screenshot-2024-12-05-at-22.06.39.png) Tip: Avoid subscribing in first place, do everything to end up with async pipe. PS: as of Angular v16 — the best way to unsubscribe is by using takeUntilDestroyed() **UPDATE December 5, 2024.** --- I have been asked long time a go to address voices saying that you should leave subscriptions without unsubscribe on component destroy when you deal with: ## PUT, PATCH, POST, DELETE Again, argument is that when you unsubscribe, you are going to potentially render user action that performed HTTP request not completed if user navigates away since subscription is going to get destroyed with component when navigating away. Cancelling the request in the process. If you decide to leave the subscription lingering relying on automatic HttpClient clean up on completion, this HTTP request is going to complete eventually. These operations are “one-and-done,” as the arguments claim. Technically this is 100% correct. Fully agreed here - this is how it works. But let’s stop pretending this is realistic. In real-world apps, things don’t always go as planned. Users navigate away, components are destroyed, requests hang, and your UI ends up in a mess. Here’s why the argument against unsubscribing is good for a demo and lab like projects ### **Real users are impatient** - They’ll click, tab, and navigate faster than your HTTP response can complete. If you don’t clean up, you’ll end up with weird state updates firing after a component is gone. ### **Consistency matters** - Treating some observables differently creates unnecessary complexity. Just use `takeUntilDestroyed` everywhere and stop thinking about exceptions. ### **AbortController exists for a reason.** - Modern JavaScript provides `AbortController`, which lets you cleanly cancel HTTP requests directly at the browser level. While not Angular-specific, it fits perfectly with Angular's `takeUntilDestroyed` end result. ### **"One-and-done” is a myth.** - Backend failures, race conditions, or retries can all make your “simple” PUT or POST turn into a debugging nightmare. Value consistency. Value simplicity. Unsubscribe 🤝 ### UPDATE |11 [FREE] E-Books - Angular Enterprise Architecture by Tomas Trajan URL: https://www.angularspace.com/11-e-books-giveaway/ Last updated: 2024-04-23T20:12:42.000Z \[UPDATE\] > Angular Enterprise Architecture > by [@tomastrajan](https://twitter.com/tomastrajan?ref%5Fsrc=twsrc%5Etfw&ref=angularspace.com) \- Giveaway WINNERS! > > 11 E-Books found new owners! > > Congrats to lucky ones!!! 🥳 > I'm going to contact you via e-mail.[#angular](https://twitter.com/hashtag/angular?src=hash&ref%5Fsrc=twsrc%5Etfw&ref=angularspace.com) [pic.twitter.com/Gx7IcYVErP](https://t.co/Gx7IcYVErP?ref=angularspace.com) > > — Daniel Glejzner (@DanielGlejzner) [April 8, 2024](https://twitter.com/DanielGlejzner/status/1777342118618345714?ref%5Fsrc=twsrc%5Etfw&ref=angularspace.com) \[UPDATE\] Time to start the grand giveaway series to celebrate expansion of Angular Space to another level of partial maximum capacity! Welcome to everyone who just joined! **We are now at over 1800 registered members. Next limit 2000.** _This post is for subscribers only._ ### #1 Social Media Digest - April Fools Day! URL: https://www.angularspace.com/1-social-media-digest-april-fools-day/ Last updated: 2024-04-02T00:07:00.000Z I gathered social media posts from Software Dev folks on X that they have posted to prank others on April Fools Day. _This post is for subscribers only._ ### ⭐ Writer Mentorship Program ⭐ URL: https://www.angularspace.com/angular-space-writer-mentorship-program/ Last updated: 2024-03-28T21:41:48.000Z **Announcing the Angular Space Writer Mentorship Program** ## Exclusive to Angular Space Registered Members! I'm thrilled to announce the **Angular Space Writer Mentorship Program**, where community members can transform their article ideas into educational masterpieces **with the help of industry experts.** --- ## **How It Works 👇** ### **#1 Submit Your Idea** - Use a form to **propose the article** 💡 you want to write for Angular Space ( To be published soon). - We are going to take limited amount of ideas in phases **to ensure that you are taken care of!** - If we reject your idea you are going to receive explanation why and guidelines for selecting new idea! ( **First Learning opportunity** ) 🧑‍🎓 ### **#2 Technical Review** - Once your article idea is approved, **you need to send us a draft** focusing on technical educational value you want to pass ( focus on code 😄 ). - One (or multiple) **Technical Mentors ⭐ is/are going to take care of you from now on** to validate the technical correctness of the code and the concept. Ensure that the content is suitable for an article and will truly educate our readers. - You are already in the process, **every mistake you make will be a learning opportunity 🧑‍🎓**. - You are going to cooperate with Mentors until the result is satisfying. **Mentors are going to be there to help you with every question and doubt!** ### **#3 Content Delivery Review** - After passing the Technical Review, we know the code is solid, and the technical message is educational. - Now, **Content Delivery** **Mentors** ⭐ work on clarifying descriptions, improving content delivery, adding impactful titles, and **ensuring the article is both engaging, informative and well structured.** - This is a crucial part that many don't fulfil in a correct way. **You also need to learn to sell 💰** your technical expertise so it impacts highest amount of devs along the way. More fun and engaging = more views. - "But Views and Likes are not important or a metric of good article" - correct ✅. However **without views/likes your content is going to have limited reach** and your educational effort is going to impact small amount of devs ☹️... - Once the fun and engaging part is ✅, **you attracted many to read**. Your content now get's exposed to the it's foundational core. The technical/educational part - the part you value the most! ### **#4 Final Review** - The article undergoes a comprehensive review of both its technical and content delivery elements. Final touches are added if needed and it's queued for publication! 👏 --- ## **High-Level Vision 👁️** ![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/03/Screenshot-2024-03-27-at-14.20.27.png) ### **Win-Win-Win** 🏆 - We aim for **impact**, leveraging our expertise to maximize contributions to the open-source community. Your article will not only be a learning experience for you but also a valuable asset for Angular Space and the tech community. - As you navigate through each stage of the process, you'll refine your technical knowledge, **learn from any mistakes**, and improve upon your original ideas. - Learn to write in a way that is technically sound and accessible, creating **content that readers can understand, enjoy, and trust.** - Mentors will practice and enhance their mentoring skills, **ensuring the high quality** of the content published under Angular Space's name. - Angular Space gains insightful educational content, while the tech community can rely on these articles to be **credible and valuable, thanks to the mentorship seal of approval.** **Everyone wins! Everyone gains something in the process 😃** And of course, when your article is ready for publication, not only will your name be featured as the author, but the mentors who've supported and guided you will also be acknowledged for their contributions. - You can publish your work on dev/medium etc. We do not own any rights to your content. We are going to ask you for courtesy Angular Space Mentorship Program info placement + Mentors who reviewed your article at the end when you decide to publish in different places as well. **Join us in this unique mentorship opportunity to write, learn, and make a lasting impact!** Register for Waitlist now 👇 --- ### I'm going to publish the registration form soon! Make sure to keep an eye on Angular Space, E-mails, my tweets/posts on X 😎 ### Exclusive Job Offers for Angular Space Members URL: https://www.angularspace.com/exclusive-job-offers-for-angular-space-members/ Last updated: 2024-03-20T16:26:19.000Z ## **Can't land Angular job?** ## **Angular Space is here to fix it** There is one main problem at the moment. It's hard to stand out! **\[ I have a solution \]** There are literally hundreds CVs for a single position. It's both a burden for devs and companies alike. \[ **Solution?** \] As Angular Space Member. You are going apply for exclusive job offers! Only Members are going to be able to apply for Angular Space Job offers. **\[ Not a lazy job board \]** \- Every job offer is going to be vetted by me \- Company that is going to hire, is only going to take CVs from Angular Space Members! \- Offers are going to be time boxed, if there was no candidate found within Angular Space Members. Offer is going to go out the the wider market. **This grants you a lifetime opportunity to finally stand out if you match the requirements of the offer.** **For now we are maxed out at 1000 Members.** **Soon im going to gradually open for more** **Sign up for waitlist here:** ### TanStack Form + Angular - First Look! URL: https://www.angularspace.com/first-look-tanstack-form-angular-a/ Last updated: 2024-03-17T22:50:48.000Z It's here. We have a first draft of working code for **TanStack Form Angular Adapter!** Take a look at the implementation below and **make sure to provide your feedback** on how you feel with the current proposal. Do it directly in comments on GitHub or under this post. [\[WIP\] Angular adapter by crutchcorn · Pull Request #627 · TanStack/formHi Enea This PR starts an initial Angular adapter that has an API like so: import { Component } from ‘@angular/core’ import { injectForm, TanStackField } from ‘@tanstack/angular-form’ import { NgF…![](https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg)GitHubTanStack![](https://opengraph.githubassets.com/e2e78c08dce592482e75043d59ebeef655dc4f7ec72bf4c7747333d6a22d9d34/TanStack/form/pull/627)](https://github.com/TanStack/form/pull/627?ref=angularspace.com) Code: ```js import { Component } from '@angular/core' import { injectForm, TanStackField } from '@tanstack/angular-form' import { NgFor } from '@angular/common' @Component({ selector: 'app-root', standalone: true, imports: [TanStackField, NgFor], template: `
{{ error }}
`, }) export class AppComponent { required = ({ value }: { value: string }) => { return !value ? 'Required' : undefined } form = injectForm({ defaultValues: { firstName: 'Ryan', age: 25, }, onSubmit({ value }: any) { console.log({ value }) }, }) handleSubmit(event: SubmitEvent) { event.preventDefault() event.stopPropagation() void this.form.handleSubmit() } } ``` Is this how you imagined an alternative for Template Driven/Reactive Forms? ### We have a first SPONSOR! URL: https://www.angularspace.com/we-have-a-first-sponsor/ Last updated: 2024-03-13T15:22:02.000Z **First Angular Space Sponsor!!!** Giant thanks to [@brechtbilliet](https://twitter.com/brechtbilliet?ref=angularspace.com) & [@pick\_simplified](https://twitter.com/pick%5Fsimplified?ref=angularspace.com) ! **Brecht decided to donate 500€** to help Angular Space grow and evolve. If you would like to sponsor Angular Space as well drop me a DM :) [**Checkout Simplified Courses**](https://www.simplified.courses/?ref=angularspace.com) 👈 Brecht is always dropping top notch Angular content! His latest Forms course with Signals is amazing [**More here**](https://www.simplified.courses/e-store?ref=angularspace.com) 👈 ### [Utility] Angular 16 killed NGCC - You can't simply upgrade! URL: https://www.angularspace.com/angular-16-killed-ngcc-stuck-with-old-dependencies/ Last updated: 2024-03-07T21:38:02.000Z If you have a project which is still below Angular 16 or you think you might have one in the future. I have created a **special utility that is going to help** you manage dependencies before upgrading! **View Engine dependencies, no longer compile with Angular 16+.** Angular Compatibility Compiler (**NGCC**) has been removed. If you have a lot of dependencies in your project, you have to figure out if you have some **old View Engine dependencies** for proper planning. Before upgrading to Angular 16+ **you have to get rid of them.** Checking one by one is a huge time consuming chore! I made a utility that is going to **audit your** **package.json** and tell you what needs to be **removed or upgraded** if still maintained. I'm open for PRs and if you find an issue please submit in GitHub! If you find it useful/interesting I would be grateful for a ⭐ 🙂 [GitHub - danielglejzner/ng16-dep-audit: NGCC has been removed in Angular 16\. Quickly check which dependencies stop you from upgrading!NGCC has been removed in Angular 16\. Quickly check which dependencies stop you from upgrading! - danielglejzner/ng16-dep-audit![](https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg)GitHubdanielglejzner![](https://opengraph.githubassets.com/178c78bb3b447418fb1ae508719a414f706040901026f9403b0d288cb47b0b85/danielglejzner/ng16-dep-audit)](https://github.com/danielglejzner/ng16-dep-audit?ref=angularspace.com) ## Features - Checks each npm package in your project's `package.json`. - Identifies Angular packages that need to be upgraded or removed/replaced to be compatible with Angular 16 compiler. - Lists packages that do not have Angular dependencies or are not visible in the npm registry. ## Usage - Make sure to run the command from a directory where you `package.json` is located ### Basic Command ```bash npx ng16-dep-audit ``` ### Command-Line Options - **`--style=