Document Details

Uploaded by Deleted User

Manfred Steyer

Tags

angular software development javascript framework programming

Summary

This book, Modern Angular v2, is a comprehensive guide to modern Angular development. It explores Standalone Components, Signals, and other recent features, providing examples and explanations to help readers understand and utilize these innovations for building applications. The book is structured into several parts covering different areas of modern Angular development.

Full Transcript

Modern Angular Manfred Steyer © 2022 - 2024 Manfred Steyer Contents Intro....................................................... 1 Structure.................................................. 1 Help to Improve this Book!....................................... 2 Spread the Wor...

Modern Angular Manfred Steyer © 2022 - 2024 Manfred Steyer Contents Intro....................................................... 1 Structure.................................................. 1 Help to Improve this Book!....................................... 2 Spread the Word!............................................. 3 Trainings and Consulting........................................ 3 Standalone Components: Mental Model & Compatibility..................... 5 Why Did we Even Get NgModules in the First Place?....................... 5 Getting Started With Standalone Components........................... 6 The Mental Model............................................ 7 Pipes, Directives, and Services..................................... 7 Bootstrapping Standalone Components............................... 8 Compatibility With Existing Code.................................. 9 Side Note: The CommonModule.................................... 11 Conclusion................................................. 12 Architecture with Standalone Components.............................. 13 Grouping Building Blocks........................................ 13 Importing Whole Barrels........................................ 15 Barrels with Pretty Names: Path Mappings............................. 16 Workspace Libraries and Nx...................................... 17 Module Boundaries with Sheriff.................................... 20 Conclusion................................................. 20 Standalone APIs for Routing and Lazy Loading........................... 21 Providing the Routing Configuration................................. 21 Using Router Directives......................................... 22 Lazy Loading with Standalone Components............................ 23 Environment Injectors: Services for Specific Routes........................ 25 Setting up NGRX and Feature Slices................................. 26 Setting up Your Environment: ENVIRONMENT_INITIALIZER................ 28 Component Input Bindings....................................... 28 Conclusion................................................. 29 Angular Elements with Standalone Components........................... 30 CONTENTS Providing a Standalone Component................................. 30 Installing Angular Elements...................................... 31 Bootstrapping with Angular Elements................................ 31 Side Note: Bootstrapping Multiple Components.......................... 32 Calling an Angular Element...................................... 33 Calling a Web Component in an Angular Component...................... 34 Bonus: Compiling Self-contained Bundle.............................. 35 Conclusion................................................. 36 The Refurbished HttpClient - Standalone APIs and Functional Interceptors......... 37 Standalone APIs for HttpClient.................................... 37 Functional Interceptors......................................... 38 Interceptors and Lazy Loading..................................... 39 Pitfall with withRequestsMadeViaParent.............................. 39 Legacy Interceptors and Other Features............................... 40 Further Features............................................. 41 Conclusion................................................. 41 Testing Angular Standalone Components............................... 42 Test Setup................................................. 42 The HttpClient Mock.......................................... 43 Shallow Testing.............................................. 44 Mock Router and Store......................................... 45 Conclusion................................................. 48 Patterns for Custom Standalone APIs in Angular.......................... 49 Case Study for Patterns......................................... 49 The Golden Rule............................................. 52 Pattern: Provider Factory........................................ 52 Pattern: Feature.............................................. 54 Pattern: Configuration Provider Factory............................... 57 Pattern: NgModule Bridge....................................... 60 Pattern: Service Chain.......................................... 62 Pattern: Functional Service....................................... 64 Conclusion................................................. 67 How to prepare for Standalone Components?............................. 69 Option 1: Ostrich Strategy....................................... 69 Option 2: Just Throw Away Angular Modules........................... 69 Option 2a: Automatic Migration to Standalone........................... 71 Option 3: Replace Angular Modules with Barrels......................... 71 Option 4: Nx Workspace with Libraries and Linting Rules.................... 73 Option 4a: Folder-based Module Boundaries with Sheriff.................... 77 Conclusion................................................. 77 CONTENTS Automatic Migration to Standalone Components in 3 Steps.................... 78 A First Look at the Application to Migrate............................. 78 Step 1.................................................... 79 Step 2.................................................... 80 Step 3.................................................... 82 Bonus: Moving to Standalone APIs.................................. 83 Conclusion................................................. 86 Signals in Angular: The Future of Change Detection........................ 87 Change Detection Today: Zone.js................................... 87 Change Detection Tomorrow: Signals................................ 88 Using Signals............................................... 89 Updating Signals............................................. 91 Signals Need to be Immutable..................................... 91 Calculated Values, Side Effects, and Assertions........................... 91 Effects Need an Injection Context................................... 92 Writing Signals in Effects........................................ 93 Signals and Change Detection..................................... 94 RxJS Interop................................................ 95 NGRX and Other Stores?........................................ 96 Conclusion................................................. 96 Component Communication with Signals: Inputs, Two-Way Bindings, and Content/ View Queries............................................... 97 Input Signals................................................ 97 Two-Way Data Binding with Model Signals............................ 102 Content Queries with Signals..................................... 103 Output API................................................ 106 View Queries with Signals....................................... 107 Queries and ViewContainerRef.................................... 109 Feature Parity between Content and View Queries........................ 114 Conclusion................................................. 114 Successful with Signals in Angular - 3 Effective Rules for Your Architecture........ 115 Initial Example With Some Room for Improvement........................ 115 Rule 1: Derive State Synchronously Wherever Possible...................... 118 Rule 2: Avoid Effects for Propagating State............................. 119 Rule 3: Stores Simplify Reactive Data Flow............................. 124 Conclusion................................................. 126 Built-in Control Flow and Deferrable Views............................. 128 New Syntax for Control Flow in Templates............................. 128 Automatic Migration to Build-in Control Flow........................... 130 Delayed Loading............................................. 130 CONTENTS Conclusion................................................. 133 esbuild and the new Application Builder................................ 134 Build Performance with esbuild.................................... 134 SSR Without Effort with the new Application Builder...................... 134 More than SSR: Non-destructive Hydration............................. 135 More Details on Hydration in Angular................................ 137 Conclusion................................................. 139 About the Author............................................... 140 Trainings and Consulting.......................................... 141 Intro At the beginning of 2023, Sarah Drasner, who as Director of Engineering at Google also heads the Angular team, coined the term Angular Renaissance. This term means a renewal of the framework that has supported us in the development of modern JavaScript solutions for seven years now. This renewal is incremental and backwards compatible and takes current trends from the world of front-end frameworks into account. This is primarily about developer experience and performance. Standalone Components and Signals are two well-known features that have already emerged as part of this movement. In this book, I discuss the innovations that come with the Angular Renaissance using several examples. Structure This book is subdivided into 14 chapters grouped to four parts discussing different aspects of modern Angular. Part 1: Standalone Components The first part discusses Standalone Components, how they play together with traditional NgModule- based code, and what they mean for your architecture. Chapters in part 1: Standalone Components: Mental Model & Compatibility Architecture with Standalone Components Part 2: Improved APIs This part goes in depth with the new Standalone APIs – renewed Angular APIs for routing, lazy loading, http access, web components, and testing. Chapters in part 2: Standalone APIs for Routing and Lazy Loading Angular Elements with Standalone Components The Refurbished HttpClient - Standalone APIs and Functional Interceptors Testing Angular Standalone Components Patterns for Custom Standalone APIs in Angular Intro 2 Part 3: Preparing for and Migrating to Standalone In this part you learn how to migrate your existing code to Standalone. Chapters in part 3: How to prepare for Standalone Components? Automatic Migration to Standalone Components in 3 Steps Part 4: Signals Signals are the future of change detection in Angular. The fourth part shows how to use them in your applications. Chapters in part 4: Signals in Angular: The Future of Change Detection Component Communication with Signals: Inputs, Two-Way Bindings, and Content/ View Queries Successful with Signals in Angular - 3 Effective Rules for Your Architecture Part 5: Control Flow & Performance The final part explains what’s behind the new control flow syntax, how to improve the performance with deferable views, SSR, and hydration, and how to speed up the build with the new esbuild-based ApplicationBuilder. Chapters in part 5: Built-in Control Flow and Deferrable Views esbuild and the new Application Builder Help to Improve this Book! Please let us know if you have any suggestions or find any mistakes. Just open an issue at or send a pull request to the book’s GitHub repository¹. ¹https://github.com/manfredsteyer/standalone-book.git Intro 3 Spread the Word! If you like this book, tell your contacts via Twitter/X², Facebook³, and/or LinkedIn⁴ about it. Also, feel free to send the download link⁵ to colleagues and friends via email or your company- internal chat platform like Slack or Teams. Trainings and Consulting If you and your team need support or trainings regarding Angular, we are happy to help with workshops and consulting (on-site or remote). In addition to several other kinds of workshop, we provide the following ones: Advanced Angular: Enterprise Solutions and Architecture Angular Essentials: Building Blocks and Concepts Modern Angular Workshop Angular Micro Frontends Workshop Angular Testing Workshop (Cypress, Jest, etc.) Angular Performance Workshop Angular Design Systems Workshop (Figma, Storybook, etc.) Angular: Reactive Architectures (RxJS and NGRX) Angular Review Workshop Angular Upgrade Workshop Please find the full list of our offers here⁶. ²https://twitter.com/intent/post?text=Check%20out%20this%20free%20eBook%20about%20%23ModernAngular%20by%20% 40manfredsteyer%20%0A%0Ahttps%3A%2F%2Fwww.angulararchitects.io%2Fen%2Febooks%2Fmodern-angular%2F%3Fbook ³https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.angulararchitects.io%2Fen%2Febooks%2Fmodern- angular%2F%3Fbook ⁴https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.angulararchitects.io%2Fen%2Febooks%2Fmodern- angular%2F%3Fbook ⁵https://www.angulararchitects.io/en/ebooks/modern-angular/?book ⁶https://www.angulararchitects.io/en/angular-workshops/ Intro 4 Modern Angular Workshop Modern Angular (English)⁷ | Modern Angular (German)⁸ We provide our workshops and consulting in various forms: remote or on-site; public or as dedicated company workshops; in English or in German. If you have any questions, reach out to us at [email protected]. ⁷https://www.angulararchitects.io/en/training/modern-angular-workshop/ ⁸https://www.angulararchitects.io/training/modern-angular-workshop/ Standalone Components: Mental Model & Compatibility Standalone Components is one of the most exciting new Angular features since quite a time. They allow for working without NgModules and hence are the key for more lightweight and straightforward Angular solutions. In this book, I’m going to demonstrate how to leverage this innovation. For this, I’m using an example application completely written with Standalone Components. The source code for this can be found in the form of a traditional Angular CLI workspace⁹ and as an Nx workspace¹⁰ that uses libraries as a replacement for NgModules. Why Did we Even Get NgModules in the First Place? The main reason for initially introducing NgModules was pragmatic: We needed a way to group building blocks that are used together. Not only to increase the convenience for developers, but also for the Angular Compiler whose development lagged a little behind. In the latter case, we are talking about the compilation context. From this context, the compiler learns where the program code is allowed to call which components: NgModules provide the Compilation Context However, the community was never really happy with this decision. Having another modular system besides that of EcmaScript didn’t feel right. In addition, it raised the entry barrier for new Angular developers. That is why the Angular team designed the new Ivy compiler so that the compiled application works without modules at runtime. Each component compiled with Ivy has its own ⁹https://github.com/manfredsteyer/standalone-example-cli ¹⁰https://github.com/manfredsteyer/standalone-example-nx Standalone Components: Mental Model & Compatibility 6 compilation context. Even if that sounds grandiose, this context is just represented by two arrays that refer to adjacent components, directives, and pipes. Since the old compiler and the associated execution environment have now been permanently removed from Angular as of Angular 13, it was time to anchor this option in Angular’s public API. For some time there has been a design document and an associated RFC [RFC]. Both describe a world where Angular modules are optional. The word optional is important here: Existing code that relies on modules is still supported. Getting Started With Standalone Components In general, implementing a Standalone Component is easy. Just set the standalone flag in the Component decorator to true and import everything you want to use: 1 @Component({ 2 standalone: true, 3 selector: 'app-root', 4 imports: [ 5 RouterOutlet, 6 NavbarComponent, 7 SidebarComponent, 8 ], 9 templateUrl: './app.component.html', 10 styleUrls: ['./app.component.css'] 11 }) 12 export class AppComponent { 13 [...] 14 } The imports define the compilation context: all the other building blocks the Standalone Compo- nents is allowed to use. For instance, you use it to import further Standalone Component, but also existing NgModules. The exhaustive listing of all these building blocks makes the component self-sufficient and thus increases its reusability in principle. It also forces us to think about the component’s dependencies. Unfortunately, this task turns out to be extremely monotonous and time consuming. Therefore, there are considerations to implement a kind of auto-import in the Angular Language Service used by the IDEs. Analogous to the auto-import for TypeScript modules, the IDE of choice could also suggest placing the corresponding entry in the imports array the first time a component, pipe or directive is used in the template. Standalone Components: Mental Model & Compatibility 7 The Mental Model The underlying mental model helps to better understand Standalone Components. In general, you can imagine a Standalone Component as a component with its very own NgModule: Mental Model This is similar to Lars Nielsen¹¹’s SCAM pattern. However, while SCAM uses an explicit module, here we only talk about a thought one. While this mental model is useful for understanding Angular’s behavior, it’s also important to see that Angular doesn’t implement Standalone Components that way underneath the covers. Pipes, Directives, and Services Analogous to standalone components, there are also standalone pipes and standalone directives. For this purpose, the pipe and directive decorators also get a standalone property. This is what a standalone pipe will look alike: ¹¹https://twitter.com/LayZeeDK Standalone Components: Mental Model & Compatibility 8 1 @Pipe ({ 2 standalone: true, 3 name: 'city', 4 pure: true 5 }) 6 export class CityPipe implements PipeTransform { 7 8 transform (value: string, format: string): string {[…]} 9 10 } And here is an example for a standalone directive: 1 @Directive ({ 2 standalone: true, 3 selector: 'input [appCity]', 4 providers: […] 5 }) 6 export class CityValidator implements Validator { 7 8 [...] 9 10 } Thanks to tree-shakable providers, on the other hand, services have worked without modules for quite a time. For this purpose the property providedIn has to be used: 1 @Injectable ({ 2 providedIn: 'root' 3 }) 4 export class FlightService {[…]} The Angular team recommends, to use providedIn: 'root' whenever possible. It might come as a surprise, but providedIn: 'root' also works with lazy loading: If you only use a service in lazy parts of your application, it is loaded alongside them. Bootstrapping Standalone Components Until now, modules were also required for bootstrapping, especially since Angular expected a module with a bootstrap component. Thus, this so called AppModule or “root module” defined the main component alongside its compilation context. With Standalone Components, it will be possible to bootstrap a single component. For this, Angular provides a method bootstrapApplication which can be used in main.ts: Standalone Components: Mental Model & Compatibility 9 1 // main.ts 2 3 import { bootstrapApplication } from '@angular/platform-browser'; 4 import { provideAnimations } from '@angular/platform-browser/animations'; 5 import { AppComponent } from './app/app.component'; 6 import { APP_ROUTES } from './app/app.routes'; 7 import { provideRouter } from '@angular/router'; 8 import { importProvidersFrom } from '@angular/core'; 9 10 [...] 11 12 bootstrapApplication(AppComponent, { 13 providers: [ 14 importProvidersFrom(HttpClientModule), 15 provideRouter(APP_ROUTES), 16 provideAnimations(), 17 importProvidersFrom(TicketsModule), 18 importProvidersFrom(LayoutModule), 19 ] 20 }); The first argument passed to bootstrapApplication is the main component. Here, it’s our AppComponent. Via the second argument, we pass application-wide service providers. These are the providers, you would register with the AppModule when going with NgModules. The provided helper function importProvidersFrom allows bridging the gap to existing NgModules. Please also note, that importProvidersFrom works with both NgModules but also ModuleWithProviders as returned by methods like forRoot and forChild. While this allows to immediately leverage existing NgModule-based APIs, we will see more and more functions that replace the usage of importProvidersFrom in the future. For instance, to register the router with a given configuration, the function provideRouter is used. Similarly, provideAnimations setup up the Animation module. Compatibility With Existing Code As discussed above, according to the mental model, a Standalone Component is just a component with its very own NgModule. This is also the key for the compatibility with existing code still using NgModules. On the one side, we can import whole NgModules into a Standalone Component: Standalone Components: Mental Model & Compatibility 10 1 import { Component, OnInit } from '@angular/core'; 2 import { TicketsModule } from '../tickets/tickets.module'; 3 4 @Component({ 5 selector: 'app-next-flight', 6 standalone: true, 7 imports: [ 8 // Existing NgModule imported 9 // in this standalone component 10 TicketsModule 11 ], 12 [...] 13 }) 14 export class NextFlightComponent implements OnInit { 15 [...] 16 } But on the other side, we can also import a Standalone Component (Directive, Pipe) into an existing NgModule: 1 @NgModule({ 2 imports: [ 3 CommonModule, 4 5 // Imported Standalone Component: 6 FlightCardComponent, 7 [...] 8 ], 9 declarations: [ 10 MyTicketsComponent 11 ], 12 [...] 13 }) 14 export class TicketsModule { } Interestingly, standalone components are imported like modules and not declared like classic components. This may be confusing at first glance, but it totally fits the mental model that views a standalone component a component with its very own NgModule. Also, declaring a traditional component defines a strong whole-part relationship: A traditional component can only be declared by one module and then, it belongs to this module. However, a standalone component doesn’t belong to any NgModule but it can be reused in several places. Hence, using imports here really makes sense. Standalone Components: Mental Model & Compatibility 11 Side Note: The CommonModule Doubtless, one of the most known NgModules in Angular was the CommonModule. It contains built-in directives like *ngIf or *ngFor and built-in pipes like async or json. While you can still import the whole CommonModule, meanwhile it’s also possible to just import the needed directives and pipes: 1 import { 2 AsyncPipe, 3 JsonPipe, 4 NgForOf, 5 NgIf 6 } from "@angular/common"; 7 8 [...] 9 10 @Component({ 11 standalone: true, 12 imports: [ 13 // CommonModule, 14 NgIf, 15 NgForOf, 16 AsyncPipe, 17 JsonPipe, 18 19 FormsModule, 20 FlightCardComponent, 21 CityValidator, 22 ], 23 selector: 'flight-search', 24 templateUrl: './flight-search.component.html' 25 }) 26 export class FlightSearchComponent implements OnInit { 27 [...] 28 } This is possible, because the Angular team made Standalone Directives and Standalone Pipes out of the building blocks provided by the CommonModule. Importing these building blocks in a fine grained way will be especially interesting once IDEs provide auto-imports for standalone components. In this case, the first usage of an building block like *ngIf will make the IDE to add it to the imports array. As we will see in a further part of this book, meanwhile also the RouterModule comes with Standalone building-blocks. Hence, we can directly import RouterOutlet instead of Standalone Components: Mental Model & Compatibility 12 going with the whole RouterModule. When writing this, this was not yet possible for other modules like the FormsModule or the HttpClientModule. Conclusion So far we’ve seen how to use Standalone Components to make our Angular applications more lightweight. We’ve also seen that the underlying mental model guarantees compatibility with existing code. However, now the question arises how this all will influence our application structure and architecture. The next chapter will shed some light on this. Architecture with Standalone Components In last chapter, I’ve shown how Standalone Components will make our Angular applications more lightweight in the future. In this part, I’m discussing options for improving your architecture with them. The source code for this can be found in the form of a traditional Angular CLI workspace¹² and as an Nx workspace¹³ that uses libraries as a replacement for NgModules. Grouping Building Blocks Unfortunately, the examples shown so far cannot keep up with one aspect of NgModules. Namely the possibility of grouping building blocks that are usually used together. Obviously, the easiest approach for grouping stuff that goes together is using folders. However, you might go one step further by leveraging barrels: A barrel is an EcmaScript file that exports related elements. These files are often called public-api.ts or index.ts. The example project used contains such an index.ts to group two navigation components from the shell folder: Grouping two Standalone Components with a barrel The barrel itself re-exports the two components: ¹²https://github.com/manfredsteyer/standalone-example-cli ¹³https://github.com/manfredsteyer/standalone-example-nx Architecture with Standalone Components 14 1 export { NavbarComponent } from './navbar/navbar.component'; 2 export { SidebarComponent } from './sidebar/sidebar.component'; The best of this is, you get real modularization: Everything the barrel experts can be used by other parts of your application. Everything else is your secret. You can modify these secrets as you want, as long as the public API defined by your barrel stays backwards compatible. In order to use the barrel, just point to it with an import: 1 import { 2 NavbarComponent, 3 SidebarComponent 4 } from './shell/index'; 5 6 @Component({ 7 standalone: true, 8 selector: 'app-root', 9 imports: [ 10 RouterOutlet, 11 12 NavbarComponent, 13 SidebarComponent, 14 ], 15 templateUrl: './app.component.html', 16 styleUrls: ['./app.component.css'] 17 }) 18 export class AppComponent { 19 [...] 20 } If you call your barrel index.ts, you can even omit the file name, as index is the default name when configuring the TypeScript compiler to use Node.js-based conventions. Something that is the case in the world of Angular and the CLI: Architecture with Standalone Components 15 1 import { 2 NavbarComponent, 3 SidebarComponent 4 } from './shell'; 5 6 @Component({ 7 standalone: true, 8 selector: 'app-root', 9 imports: [ 10 RouterOutlet, 11 12 NavbarComponent, 13 SidebarComponent, 14 ], 15 templateUrl: './app.component.html', 16 styleUrls: ['./app.component.css'] 17 }) 18 export class AppComponent { 19 [...] 20 } Importing Whole Barrels In the last section, the NavbarComponent and the SidebarComponent were part of the shell’s public API. Nevertheless. Angular doesn’t provide a way to import everything a barrel provides at once. In most of the cases, this is the totally fine. Auto-imports provided by your IDE will add the needed entries anyway. Also, being explicit about what you need helps enables tree-shaking. However, in some edge-cases where you know that some building blocks always go together, e. g. because there is a strong mutual dependency, putting them into an array can help to make our lives easier. For instance, think about all the directives provided by the FormsModule. Normally, we don’t even know their exact names nor which of them play together. The following example demonstrates this idea: Architecture with Standalone Components 16 1 import { NavbarComponent } from './navbar/navbar.component'; 2 import { SidebarComponent } from './sidebar/sidebar.component'; 3 4 export { NavbarComponent } from './navbar/navbar.component'; 5 export { SidebarComponent } from './sidebar/sidebar.component'; 6 7 export const SHELL = [ 8 NavbarComponent, 9 SidebarComponent 10 ]; Interestingly, such arrays remind us to the exports section of NgModules. Please note that your array needs to be a constant. This is needed because the Angular Compiler uses it already at compile time. Such arrays can be directly put into the imports array. No need for spreading them: 1 import { SHELL } from './shell'; 2 3 [...] 4 5 @Component({ 6 standalone: true, 7 selector: 'app-root', 8 imports: [ 9 RouterOutlet, 10 11 // NavbarComponent, 12 // SidebarComponent, 13 SHELL 14 ], 15 templateUrl: './app.component.html', 16 styleUrls: ['./app.component.css'] 17 }) 18 export class AppComponent { 19 [...] 20 } One more time I want to stress out that this array-based style should only be used with caution. While it allows to group things that always go together it also makes your code less tree-shakable. Barrels with Pretty Names: Path Mappings Just using import statements that directly point to other parts of your application often leads to long relative and confusing paths: Architecture with Standalone Components 17 1 import { SHELL } from '../../../../shell'; 2 3 @Component ({ 4 standalone: true, 5 selector: 'app-my-cmp', 6 imports: [ 7 SHELL, 8 [...] 9 ] 10 }) 11 export class MyComponent { 12 } To bypass this, you can define path mappings for your barrels you import from in your TypeScript configuration (tsconfig.json in the project’s root): 1 "paths": { 2 "@demo/shell": ["src/app/shell/index.ts"], 3 [...] 4 } This allows direct access to the barrel using a well-defined name without having to worry about - sometimes excessive - relative paths: 1 // Import via mapped path: 2 import { SHELL } from '@demo/shell'; 3 4 @Component ({ 5 standalone: true, 6 selector: 'app-root', 7 imports: [ 8 SHELL, 9 [...] 10 ] 11 }) 12 export class MyComponent { 13 } Workspace Libraries and Nx These path mappings can of course be created manually. However, it is a little easier with the CLI extension Nx¹⁴ which automatically generates such path mappings for each library created within a ¹⁴https://nx.dev/ Architecture with Standalone Components 18 workspace. Libraries seem to be the better solution anyway, especially since they subdivide it more and Nx prevents bypassing the barrel of a library. This means that every library consists of a public – actually published – and a private part. The library’s public and private APIs are also mentioned here. Everything the library exports through its barrel is public. The rest is private and therefore a “secret” of the library that other parts of the application cannot access. It is precisely these types of “secrets” that are a simple but effective key to stable architectures, especially since everything that is not published can easily be changed afterwards. The public API, on the other hand, should only be changed with care, especially since a breaking change can cause problems in other areas of the project. An Nx project (workspace) that represents the individual sub-areas of the Angular solution as libraries could use the following structure: Structure of an Nx Solution Each library receives a barrel that reflects the public API. The prefixes in the library names reflect a categorization recommended by the Nx team. For example, feature libraries contain smart components that know the use cases, while UI libraries contain reusable dumb components. The domain library comes with the client-side view of our domain model and the services operating on it, and utility libraries contain general auxiliary functions. On the basis of such categories, Nx allows the definition of linting rules that prevent undesired access between libraries. For example, you could specify that a domain library should only have access to utility libraries and not to UI libraries: Architecture with Standalone Components 19 Nx prevents unwanted access between libraries via linting In addition, Nx allows the dependencies between libraries to be visualized: Nx visualizes the dependencies between libraries If you want to see all of this in action, feel free to have a look at the Nx version of the example used here. Your find the Source Code at GitHub¹⁵. ¹⁵https://github.com/manfredsteyer/demo-nx-standalone Architecture with Standalone Components 20 Besides enforcing module boundaries, Nx also comes with several additional important features: It allows for an incremental CI/CD that only rebuilds and retests parts of the monorepo that have been actually affected by a code change. Also, together with the Nx Cloud it allows to automatically parallelize the whole CI/CD process. Also, it comes with integrations into several useful tools like Storybook, Cypress, or Playwright. Module Boundaries with Sheriff Similar to Nx, the open source project Sheriff¹⁶ also allows to enforce module boundaries. However, instead of using libraries for defining modules, it just goes with folders. This makes the application structure more lightweight. Technically, Sheriff is used as via an eslint plugin. It works with traditional Angular CLI projects but also with Nx. We often combine it with Nx to get the best of both worlds: Incremental CI/CD provided by Nx and lightweight folder-based Module boundaries provided by Sheriff. Conclusion Standalone Components make the future of Angular applications more lightweight. We don’t need NgModules anymore. Instead, we just use EcmaScript modules. This makes Angular solutions more straightforward and lowers the entry barrier into the world of the framework. Thanks to the mental model, which regards standalone components as a combination of a component and a NgModule, this new form of development remains compatible with existing code. For the grouping of related building blocks, simple barrels are ideal for small solutions. For larger projects, the transition to monorepos as offered by the CLI extension Nx seems to be the next logical step. Libraries subdivide the overall solution here and offer public APIs based on barrels. In addition, dependencies between libraries can be visualized and avoided using linting. ¹⁶https://github.com/softarc-consulting/sheriff Standalone APIs for Routing and Lazy Loading Since its first days, the Angular Router has always been quite coupled to NgModules. Hence, one question that comes up when moving to Standalone Components is: How will routing and lazy loading work without NgModules? This chapter provides answers and also shows, why the router will become more important for Dependency Injection. The source code for the examples used here can be found in the form of a traditional Angular CLI workspace¹⁷ and as an Nx workspace¹⁸ that uses libraries as a replacement for NgModules. Providing the Routing Configuration When bootstrapping a standalone component, we can provide services for the root scope. These are services you used to provide in your AppModule. Meanwhile, the Router provides a function provideRouter that returns all providers we need to register here: 1 // main.ts 2 3 import { importProvidersFrom } from '@angular/core'; 4 import { bootstrapApplication } from '@angular/platform-browser'; 5 import { 6 PreloadAllModules, 7 provideRouter, 8 withDebugTracing, 9 withPreloading, 10 withRouterConfig 11 } 12 from '@angular/router'; 13 14 import { APP_ROUTES } from './app/app.routes'; 15 [...] 16 17 bootstrapApplication(AppComponent, { ¹⁷https://github.com/manfredsteyer/standalone-example-cli ¹⁸https://github.com/manfredsteyer/standalone-example-nx Standalone APIs for Routing and Lazy Loading 22 18 providers: [ 19 importProvidersFrom(HttpClientModule), 20 provideRouter(APP_ROUTES, 21 withPreloading(PreloadAllModules), 22 withDebugTracing(), 23 ), 24 25 [...] 26 27 importProvidersFrom(TicketsModule), 28 provideAnimations(), 29 importProvidersFrom(LayoutModule), 30 ] 31 }); The function provideRouter not only takes the root routes but also the implementation of additional router features. These features are passed with functions having the naming pattern withXYZ, e. g. withPreloading or withDebugTracing. As functions can easily be tree-shaken, this design decisions makes the whole router more tree-shakable. With the discussed functions, the Angular team also introduces a naming pattern, library authors should follow. Hence, when adding a new library, we just need to look out for an provideXYZ and for some optional withXYZ functions. As currently not every library comes with a provideXYZ function yet, Angular comes with the bridging function importProvidersFrom. It allows to get hold of all the providers defined in existing NgModules and hence is the key for using them with Standalone Components. I’m quite sure, the usage of importProvidersFrom will peak off over time, as more and more libraries will provide functions for directly configuring their providers. For instance, NGRX recently introduced a provideStore and a provideEffects function. Using Router Directives After setting up the routes, we also need to define a placeholder where the Router displays the activated component and links for switching between them. To get the directives needed for this, you might directly import the RouterModule into your Standalone Component. However, a better alternative is to just import the directives you need: Standalone APIs for Routing and Lazy Loading 23 1 @Component({ 2 standalone: true, 3 selector: 'app-root', 4 imports: [ 5 // Just import the RouterModule: 6 // RouterModule, 7 8 // Better: Just import what you need: 9 RouterOutlet, 10 RouterLinkWithHref, // Angular 14 11 // RouterLink // Angular 15+ 12 13 NavbarComponent, 14 SidebarComponent, 15 ], 16 templateUrl: './app.component.html', 17 styleUrls: ['./app.component.css'] 18 }) 19 export class AppComponent { 20 [...] 21 } Just importing the actually needed directives is possible, because the router exposes them as Standalone Directives. Please note that in Angular 14, RouterLinkWithHref is needed if you use routerLink with an a-tag; in all other cases you should import RouterLink instead. As this is a bit confusing, the Angular Team refactored this for Angular 15: Beginning with this version, RouterLink is used in all cases. In most cases, this is nothing we need to worry about when IDEs start providing auto-imports for Standalone Components. Lazy Loading with Standalone Components In the past, a lazy route pointed to an NgModule with child routes. As there are no NgModules anymore, loadChildren can now directly point to a lazy routing configuration: Standalone APIs for Routing and Lazy Loading 24 1 // app.routes.ts 2 3 import { Routes } from '@angular/router'; 4 import { HomeComponent } from './home/home.component'; 5 6 export const APP_ROUTES: Routes = [ 7 { 8 path: '', 9 pathMatch: 'full', 10 redirectTo: 'home' 11 }, 12 { 13 path: 'home', 14 component: HomeComponent 15 }, 16 17 // Option 1: Lazy Loading another Routing Config 18 { 19 path: 'flight-booking', 20 loadChildren: () => 21 import('./booking/flight-booking.routes') 22.then(m => m.FLIGHT_BOOKING_ROUTES) 23 }, 24 25 // Option 2: Directly Lazy Loading a Standalone Component 26 { 27 path: 'next-flight', 28 loadComponent: () => 29 import('./next-flight/next-flight.component') 30.then(m => m.NextFlightComponent) 31 }, 32 [...] 33 ]; This removes the indirection via an NgModule and makes our code more explicit. As an alternative, a lazy route can also directly point to a Standalone Component. For this, the above shown loadComponent property is used. I expect that most teams will favor the first option, because normally, an application needs to lazy loading several routes that go together. Standalone APIs for Routing and Lazy Loading 25 Environment Injectors: Services for Specific Routes With NgModules, each lazy module introduced a new injector and hence a new injection scope. This scope was used for providing services only needed by the respective lazy chunk. To cover such use cases, the Router now allows for introducing providers for each route. These services can be used by the route in question and their child routes: 1 // booking/flight-booking.routes.ts 2 3 export const FLIGHT_BOOKING_ROUTES: Routes = [{ 4 path: '', 5 component: FlightBookingComponent, 6 providers: [ 7 provideBookingDomain(config) 8 ], 9 children: [ 10 { 11 path: '', 12 pathMatch: 'full', 13 redirectTo: 'flight-search' 14 }, 15 { 16 path: 'flight-search', 17 component: FlightSearchComponent 18 }, 19 { 20 path: 'passenger-search', 21 component: PassengerSearchComponent 22 }, 23 { 24 path: 'flight-edit/:id', 25 component: FlightEditComponent 26 } 27 ] 28 }]; As shown here, we can provide services for several routes by grouping them as child routes. In these cases, a component-less parent route with an empty path (path: '') is used. This pattern is already used for years to assign Guards to a group of routes. Technically, using adding a providers array to a router configuration introduces a new injector at the level of the route. Such an injector is called Environment Injector and replaces the concept of the Standalone APIs for Routing and Lazy Loading 26 former (Ng)Module Injectors. The root injector and the platform injector are further Environment Injectors. Interestingly, this also decouples lazy loading from introducing further injection scopes. Previously, each lazy NgModule introduced a new injection scope, while non-lazy NgModules never did. Now, lazy loading itself doesn’t influence the scopes. Instead, now, you define new scopes by adding a providers array to your routes, regardless if the route is lazy or not. The Angular team recommends to use this providers array with caution and to favor providedIn: 'root' instead. As already mentioned in a previous chapter, also providedIn: 'root' allows for lazy loading. If you just use a services provided with providedIn: 'root' in lazy parts of your application, they will only be loaded together with them. However, there is one situation where providedIn: 'root' does not work and hence the providers array shown is needed, namely if you need to pass a configuration to a library. I’ve already indicated this in the above example by passing a config object to my custom provideBookingDomain. The next section provides a more elaborated example for this using NGRX. Setting up NGRX and Feature Slices To illustrate how to use libraries adopted for Standalone Components with lazy loading, let’s see how to setup NGRX. Let’s start with providing the needed global services: 1 import { bootstrapApplication } from '@angular/platform-browser'; 2 3 import { provideStore } from '@ngrx/store'; 4 import { provideEffects } from '@ngrx/effects'; 5 import { provideStoreDevtools } from '@ngrx/store-devtools'; 6 7 import { reducer } from './app/+state'; 8 9 [...] 10 11 bootstrapApplication(AppComponent, { 12 providers: [ 13 importProvidersFrom(HttpClientModule), 14 provideRouter(APP_ROUTES, 15 withPreloading(PreloadAllModules), 16 withDebugTracing(), 17 ), 18 19 // Setup NGRX: 20 provideStore(reducer), Standalone APIs for Routing and Lazy Loading 27 21 provideEffects([]), 22 provideStoreDevtools(), 23 24 importProvidersFrom(TicketsModule), 25 provideAnimations(), 26 importProvidersFrom(LayoutModule), 27 ] 28 }); For this, we go with the functions provideStore, provideEffects, and provideStoreDevtools NGRX comes with since version 14.3. To allow lazy parts of the application to have their own feature slices, we call provideState and provideEffects in the respective routing configuration: 1 import { provideEffects } from "@ngrx/effects"; 2 import { provideState } from "@ngrx/store"; 3 4 export const FLIGHT_BOOKING_ROUTES: Routes = [{ 5 path: '', 6 component: FlightBookingComponent, 7 providers: [ 8 provideState(bookingFeature), 9 provideEffects([BookingEffects]) 10 ], 11 children: [ 12 { 13 path: 'flight-search', 14 component: FlightSearchComponent 15 }, 16 { 17 path: 'passenger-search', 18 component: PassengerSearchComponent 19 }, 20 { 21 path: 'flight-edit/:id', 22 component: FlightEditComponent 23 } 24 ] 25 }]; While provideStore sets up the store at root level, provideState sets up additional feature slices. For this, you can provide a feature or just a branch name with a reducer. Interestingly, the function Standalone APIs for Routing and Lazy Loading 28 provideEffects is used at the root level but also at the level of lazy parts. Hence, it provides the initial effects but also effects needed for a given feature slice. Setting up Your Environment: ENVIRONMENT_INITIALIZER Some libraries used the constructor of lazy NgModule for their initialization. To further support this approach without NgModules, there is now the concept of an ENVIRONMENT_INITIALIZER: 1 export const FLIGHT_BOOKING_ROUTES: Routes = [{ 2 path: '', 3 component: FlightBookingComponent, 4 providers: [ 5 importProvidersFrom(StoreModule.forFeature(bookingFeature)), 6 importProvidersFrom(EffectsModule.forFeature([BookingEffects])), 7 { 8 provide: ENVIRONMENT_INITIALIZER, 9 multi: true, 10 useValue: () => inject(InitService).init() 11 } 12 ], 13 children: [ 14 [...] 15 ] 16 } Basically, the ENVIRONMENT_INITIALIZER provides a function executed when the Environment Injector is initialized. The flag multi: true already indicates that you can have several such initializers per scope. Component Input Bindings The router has also received a few nice roundings. For example, it can now be instructed to pass routing parameters directly to inputs of the respective component. For example, if a route is called with ;q=Graz, the router assigns the value Graz to the input with the name q: 1 @Input ( ) q = '' ; Retrieving the parameter values via the ActivatedRoute service is no longer necessary. This behavior applies to parameters in the data object, in the query string, as well as to the matrix parameters Standalone APIs for Routing and Lazy Loading 29 that are usual in Angular. In the event of a conflict, this order also applies, e.g. if present, the value is taken from the data object, otherwise the query string is checked and then the matrix parameters. In order not to disrupt existing code, this option must be explicitly activated. For this, the withComponentInputBinding function is used when calling provideRouter: 1 provideRouter( 2 APP_ROUTES, 3 withComponentInputBinding() 4 ), In addition, the router now has a lastSuccessfulNavigation property that provides information about the current route: 1 router = inject(Router); 2 […] 3 console.log( 4 'lastSuccessfullNavigation', 5 this.router.lastSuccessfulNavigation 6 ); Conclusion The streamlined router API removes unnecessary indirections for lazy loading: Instead of pointing to a lazy NgModule, a routing configuration now directly points to another lazy routing configuration. Providers we used to register with lazy NgModules, e.g. providers for a feature slice, are directly added to the respective route and can also be used in every child route. Angular Elements with Standalone Components Since Angular 14.2, it’s possible to use Standalone Components as Angular Elements. In this chapter, I’m going to show you, how this new feature works. Source Code¹⁹ Providing a Standalone Component The Standalone Component I’m going to use here is a simple Toggle Button called ToggleComponent: 1 import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/\ 2 core'; 3 import { CommonModule } from '@angular/common'; 4 5 @Component({ 6 selector: 'app-toggle', 7 standalone: true, 8 imports: [], 9 template: ` 10 11 Toggle! 12 13 `, 14 styles: [` 15.toggle { 16 padding:10px; 17 border: solid black 1px; 18 cursor: pointer; 19 display: inline 20 } 21 22.active { 23 background-color: lightsteelblue; 24 } ¹⁹https://github.com/manfredsteyer/standalone-components-elements Angular Elements with Standalone Components 31 25 `], 26 encapsulation: ViewEncapsulation.ShadowDom 27 }) 28 export class ToggleComponent { 29 30 @Input() active = false; 31 @Output() change = new EventEmitter(); 32 33 toggle(): void { 34 this.active = !this.active; 35 this.change.emit(this.active); 36 } 37 38 } By setting encapsulation to ViewEncapsulation.ShadowDom, I’m making the browser to use “real” Shadow DOM instead of Angular’s emulated counterpart. However, this also means that we have to use the Browser’s slot API for content projection instead of Angular’s ng-content. Installing Angular Elements While Angular Elements is directly provided by the Angular team, the CLI doesn’t install it. Hence, we need to do this by hand: 1 npm i @angular/elements In former days, @angular/elements also supported ng add. This support came with a schematic for adding a needed polyfill. However, meanwhile, all browsers supported by Angular can deal with Web Components natively. Hence, there is no need for such a polyfill anymore and so the support for ng add was already removed some versions ago. Bootstrapping with Angular Elements Now, let’s bootstrap our application and expose the ToggleComponent as a Web Component (Custom Element) with Angular Elements. For this, we can use the function createApplication added with Angular 14.2: Angular Elements with Standalone Components 32 1 // main.ts 2 3 import { createCustomElement } from '@angular/elements'; 4 import { createApplication } from '@angular/platform-browser'; 5 import { ToggleComponent } from './app/toggle/toggle.component'; 6 7 (async () => { 8 9 const app = await createApplication({ 10 providers: [ 11 12 ], 13 }); 14 15 const toogleElement = createCustomElement(ToggleComponent, { 16 injector: app.injector, 17 }); 18 19 customElements.define('my-toggle', toogleElement); 20 21 })(); We could pass an array with providers to createApplication. This allows to provide services like the HttpClient via the application’s root scope. In general, this option is needed when we want to configure these providers, e. g. with a forRoot method or a provideXYZ function. In all other cases, it’s preferable to just go with tree-shakable providers (providedIn: 'root'). The result of createApplication is a new ApplicationRef. We can pass its Injector alongside the ToggleComponent to createCustomElement. The result is a custom element that can be registered with the browser using customElements.define. Please note that the current API does not allow for setting an own zone instance like the noop zone. Instead, the Angular team wants to concentrate on new features for zone-less change detection in the future. Side Note: Bootstrapping Multiple Components The API shown also allows to create several custom elements: Angular Elements with Standalone Components 33 1 const element1 = createCustomElement(ThisComponent, { 2 injector: app.injector, 3 }); 4 5 const element2 = createCustomElement(ThatComponent, { 6 injector: app.injector, 7 }); Besides working with custom elements, the ApplicationRef at hand also allows for bootstrapping several components as Angular applications: 1 app.injector.get(NgZone).run(() => { 2 app.bootstrap(ToggleComponent, 'my-a'); 3 app.bootstrap(ToggleComponent, 'my-b'); 4 }); When bootstrapping a component this way, one can overwrite the selector to use. Please note, that one has to call bootstrap within a zone in order to get change detection. Bootstrapping several components was originally done by placing several components in your AppModule’s bootstrap array. The bootstrapApplication function used for bootstrapping Stan- dalone Components does, however, not allow for this as the goal was to provide a simple API for the most common use case. Calling an Angular Element To call our Angular Element, we just need to place a respective tag in our index.html: 1 Standalone Angular Element Demo 2 Click me! As a custom element is threaded by the browser as a normal DOM node, we can use traditional DOM calls to set up events and to assign values to properties: Angular Elements with Standalone Components 34 1 2 const myToggle = document.getElementById('myToggle'); 3 4 myToggle.addEventListener('change', (event) => { 5 console.log('active', event.detail); 6 }); 7 8 setTimeout(() => { 9 myToggle.active = true; 10 }, 3000); 11 Calling a Web Component in an Angular Component If we call a web component within an Angular component, we can directly data bind to it using brackets for properties and parenthesis for events. This works regardless whether the web component was created with Angular or not. To demonstrate this, let’s assume we have the following AppComponent: 1 import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 3 @Component({ 4 selector: 'app-root', 5 standalone: true, 6 schemas: [CUSTOM_ELEMENTS_SCHEMA], 7 template: ` 8 Root Component 9 12 Hello! 13 14 `, 15 }) 16 export class AppComponent { 17 active = false; 18 change(event: Event) { 19 const customEvent = event as CustomEvent; 20 console.log('active', customEvent.detail); 21 } 22 } Angular Elements with Standalone Components 35 This Standalone Component calls our my-toggle web component. While the Angular compiler is aware of all possible Angular components, it doesn’t know about web components. Hence, it would throw an error when seeing the my-toggle tag. To avoid this, we need to register the CUSTOM_ELEMENTS_SCHEMA schema. Before, we did this with all the NgModules we wanted to use together with Web Components. Now, we can directly register this schema with Standalone Components. Technically, this just disables the compiler checks regarding possible tag names. This is binary - the checks are either on or off – and there is no way to directly tell the compiler about the available web components. To make this component appear on our page, we need to bootstrap it: 1 // main.ts 2 3 [...] 4 // Register web components... 5 [...] 6 7 app.injector.get(NgZone).run(() => { 8 app.bootstrap(AppComponent); 9 }); Also, we need to add an element for AppComponent to the index.html: 1 Bonus: Compiling Self-contained Bundle Now, let’s assume, we only provide a custom element and don’t bootstrap our AppComponent. In order to use this custom element in other applications, we need to compile it into a self contained bundle. While the traditional webpack-based builder emits several bundles, e. g. a main bundle and a runtime bundle, the new esbuild-based ApplicationBuilder (see chapter esbuild and the new Application Builder) just gives us one bundle for our source code and another one for the polyfills. The resulting bundles look like this: 1 948 favicon.ico 2 703 index.html 3 100 177 main.43BPAPVS.js 4 33 916 polyfills.M7XCYQVG.js 5 0 styles.VFXLKGBH.css If you use your web component in an other web site, e. g. a CMS-driven one, just reference the main bundle there and add a respective tag. Also, reference the polyfills. However, when using several such bundles, you have to make sure, you only load the polyfills once. Angular Elements with Standalone Components 36 Conclusion As a by-product of Standalone Components, Angular provides a streamlined way for using Angular Elements: We start with creating an ApplicationRef to get an Injector. Alongside a Standalone Component, we pass this injector to Angular Elements. The result is a Web Component we can register with the browser. The Refurbished HttpClient - Standalone APIs and Functional Interceptors Without any doubt, the HttpClient is one of the best-known services included in Angular. For version 15, the Angular team has now adapted it for the new standalone components. On this occasion, the interceptor concept was also revised. In this chapter, I will describe these innovations. Source Code²⁰ Standalone APIs for HttpClient Beginning with version 15, the HttpClient can be set up without any reference to the HttpClientModule. Instead, we can use provideHttpClient when bootstrapping our application: 1 import { provideHttpClient, withInterceptors } from "@angular/common/http"; 2 3 [...] 4 5 bootstrapApplication(AppComponent, { 6 providers: [ 7 provideHttpClient( 8 withInterceptors([authInterceptor]), 9 ), 10 ] 11 }); This new function also enables optional features of the HttpClient. Each feature has its own function. For example, the withInterceptors function enables support for Http Interceptors. The combination of a provideXYZ function and several optional withXYZ functions is not chosen arbitrarily here but corresponds to a pattern that the Angular team generally provides for standalone APIs. Application developers must therefore be on the lookout for functions that start with provide or with when setting up a new library. ²⁰https://github.com/manfredsteyer/standalone-example-cli.git The Refurbished HttpClient - Standalone APIs and Functional Interceptors 38 Also, this pattern leads to a very pleasant side effect: libraries become more tree-shakable. This is because a static source code analysis makes it very easy to find out whether the application ever calls a function. In the case of methods, this is not so easy due to the possibility of polymorphic use of the underlying objects. Functional Interceptors When introducing standalone APIs, the Angular team also took the opportunity and revised the HttpClient a bit. One result of this are the new functional interceptors. They allow interceptors to be expressed as simple functions. A separate service that implements a predefined interface is no longer necessary: 1 import { HttpInterceptorFn } from "@angular/common/http"; 2 import { tap } from "rxjs"; 3 4 export const authInterceptor: HttpInterceptorFn = (req, next) => { 5 6 console.log('request', req.method, req.url); 7 console.log('authInterceptor') 8 9 if (req.url.startsWith('https://demo.angulararchitects.io/api/')) { 10 11 // Setting a dummy token for demonstration 12 13 constheaders = req.headers.set('Authorization', 'Bearer Auth-1234567'); 14 15 req = req.clone({ 16 headers 17 }); 18 19 } 20 21 return next(req).pipes( 22 tap(resp => console.log('response', resp)) 23 ); 24 25 } The interceptor shown adds an example security token to HTTP calls that are directed to specific URLs. Except that the interceptor is now a function of type HttpInterceptorFn, the basic function- ality of this concept has not changed. As shown above, functional interceptors can be set up using withInterceptors when calling provideHttpClient. The Refurbished HttpClient - Standalone APIs and Functional Interceptors 39 Interceptors and Lazy Loading Interceptors in lazy modules have always led to confusion: As soon as a lazy module introduces its own interceptors, those of outer scopes – e.g. the root scope – are no longer triggered. Even if modules with standalone components and APIs are a thing of the past, the basic problem remains, especially since (lazy) route configurations can now set up their own services: 1 export const FLIGHT_BOOKING_ROUTES: Routes = [{ 2 paths: '', 3 component: FlightBookingComponent, 4 providers: [ 5 MyService, 6 provideState(bookingFeature), 7 provideEffects([BookingEffects]) 8 provideHttpClient( 9 withInterceptors([bookingInterceptor]), 10 withRequestsMadeViaParent(), 11 ), 12 ], 13 }]; These services correspond to those the application previously registered in lazy modules. Technically, Angular introduces its own injector whenever such a providers array is available. This so-called environment injector defines a scope for the current route and its child routes. The new provideHttpClient function can also be used in this providers array to register intercep- tors for the respective lazy part of the application. By default, the previously discussed rule applies: If there are interceptors in the current environment injector, Angular ignores the interceptors in outer scopes. Exactly this behavior can be changed with withRequestsMadeViaParent: This method causes Angular to also trigger interceptors in outer scopes. Pitfall with withRequestsMadeViaParent The discussed withRequestsMadeViaParent function comes with a non-obvious pitfall: a root-scope service is unaware of inner scope and the interceptors registered there. It always accesses the HttpClient in the root scope and therefore only the interceptors set up there are executed: The Refurbished HttpClient - Standalone APIs and Functional Interceptors 40 Interceptors in multiple scopes To solve this problem, the application could also register the outer service in the providers array of the route configuration and thus in the inner scope. In general, however, it seems to be very difficult to keep track of such constellations. Therefore, it might make sense to do without interceptors in inner scopes altogether. As an alternative, a very generic interceptor in the root scope could be used. Such an interceptor may even load additional logic with a dynamic import from lazy applications parts. Legacy Interceptors and Other Features While the new functional interceptors are very charming, applications can still take advantage of the original class-based interceptors. This option can be enabled using the withLegacyInterceptors feature. Then, the class-based interceptors are to be registered as usual via a multi-provider: 1 bootstrapApplication(AppComponent, { 2 providers: [ 3 provideHttpClient( 4 withInterceptors([authInterceptor]), 5 withLegacyInterceptors(), 6 ), 7 { 8 provide: HTTP_INTERCEPTORS, 9 useClass: LegacyInterceptor, 10 multiple: true, The Refurbished HttpClient - Standalone APIs and Functional Interceptors 41 11 }, 12 ] 13 }); Further Features The HttpClient has some additional features that can also be activated using with-functions: withJsonpSupport, for example, activates support for JSONP, and withXsrfConfiguration configures details on the use of XSRF tokens. If the application does not call withXsrfConfiguration, default settings are used. However, to completely disable the use of XSRF tokens, call withNoXsrfProtection. Conclusion The revised HttpClient now wonderfully harmonizes with standalone components and associated concepts such as environment injectors. The Angular team also took the opportunity to revise the interceptors: They can now be implemented in the form of simple functions. In addition, it is now also possible to consider interceptors in outer scopes. Testing Angular Standalone Components With Standalone Components, Angular becomes a lot more lightweight: NgModules are optional and hence we can work with lesser indirections. To make this possible, components now refer directly to their dependencies: other components, but also directives and pipes. There are so-called Standalone APIs for configuring services such as the HttpClient. Additional Standalone APIs provide mocks for test automation. Here, I’m going to present these APIs. For this, I focus on on-board tools supplied with Angular. The examples²¹ used can be found here²² If you don’t want to use the on-board resources alone, you will find the same examples based on the new Cypress Component Test Runner and on Testing Library in the third-party-testing branch. Test Setup Even though Standalone Components make modules optional, the TestBed still comes with a testing module. It takes care of the test setup and provides all components, directives, pipes, and services for the test: 1 import { provideHttpClient } from '@angular/common/http'; 2 import { HttpTestingController, provideHttpClientTesting } 3 from '@angular/common/http/testing'; 4 5 […] 6 7 describe('FlightSearchComponent', () => { 8 let component: FlightSearchComponent; 9 let fixture: ComponentFixture; 10 beforeEach(async () => { 11 12 await TestBed.configureTestingModule({ 13 imports: [ FlightSearchComponent ], 14 providers: [ 15 provideHttpClient(), ²¹https://github.com/manfredsteyer/standalone-example-cli.git ²²https://github.com/manfredsteyer/standalone-example-cli.git Testing Angular Standalone Components 43 16 provideHttpClientTesting(), 17 18 provideRouter([]), 19 20 provideStore(), 21 provideState(bookingFeature), 22 provideEffects(BookingEffects), 23 ], 24 }) 25.compileComponents(); 26 27 fixture = TestBed.createComponent(FlightSearchComponent); 28 component = fixture.componentInstance; 29 fixture.detectChanges(); 30 }); 31 32 it('should search for flights', () => { […] }); 33 }); The example shown imports the Standalone Component to be tested and provides the required services via the providers array. This is exactly where the mentioned Standalone APIs come into play. They provide the services for the HttpClient, the router and NGRX. The provideStore function sets up the NGRX store, provideState provides a feature slice required for the test, and provideEffects registers an associated effect. Below we will swap out these constructs for mocks. The provideHttpClientTesting method is interesting: it overrides the HttpBackend used behind the scenes by the HttpClient with an HttpTestingBackend that simulates HTTP calls. It should be noted that it must be called after (!) provideHttpClient. It is therefore first necessary to set up the HttpClient by default in order to then overwrite individual details for testing. This is a pattern we will see again below when testing the router. The HttpClient Mock Once the HttpClient and HttpTestingBackend have been set up, the individual tests are imple- mented as usual: The test uses the HttpTestingController to find out about pending HTTP requests and to specify the HTTP responses to be simulated: Testing Angular Standalone Components 44 1 it('should search for flights', () => { 2 component.from = 'Paris'; 3 component.to = 'London'; 4 component.search(); 5 6 const ctrl = TestBed.inject(HttpTestingController); 7 8 const req = ctrl.expectOne('https://[…]/flight?from=Paris&to=London'); 9 req.flush([{}, {}, {}]); // return 3 empty objects as dummy flights 10 11 component.flights$.subscribe(flights => { 12 expect(flights.length).toBe(3); 13 }); 14 15 ctrl.verify(); 16 }); The test then checks whether the component processed the simulated HTTP response as intended. In the case shown, the test assumes that the component offers the received flights via its flights property. At the end, the test ensures that there are no further HTTP requests that have not yet been answered. To do this, it calls the verify method provided by the HttpTestingController. If there are still open requests at this point, verify throws an exception that causes the test to fail. Shallow Testing If you test a component, all sub-components, directives, and pipes used in the template are automatically tested as well. This is undesirable, especially for unit tests that focus on a single code unit. Also, this behavior slows down test execution when there are many dependencies. Shallow tests are used to prevent this. This means that the test setup replaces all dependencies with mocks. These mocks must have the same interface as the replaced dependencies. In the case of components, this means – among other things – that the same properties and events (inputs and outputs) must be offered, but also that the same selectors must be used. The TestBed offers the overrideComponent method for exchanging these dependencies: Testing Angular Standalone Components 45 1 await TestBed.configureTestingModule([…]) 2.overrideComponent(FlightSearchComponent, { 3 remove: { imports: [ FlightCardComponent ] }, 4 add: { imports: [ FlightCardMock ] } 5 }) 6.compileComponents(); In the case shown, the FlightSearchComponent uses another Standalone Component in its template: the FlightCardComponent. Technically, this means that the FlightCardComponent appears in the imports array of FlightSearchComponent. For implementing a shallow Test, this entry is removed. As a replacement, the FlightCardMock is added. The remove and add methods take care of this task. The FlightSearchComponent is thus used in the test without real dependencies. Nevertheless, the test can check whether components behave as desired. For example, the following listing checks whether the FlightSearchComponent sets up an element named flight-card for each flight found. 1 it('should display a flight-card for each found flight', () => { 2 component.from = 'Paris'; 3 component.to = 'London'; 4 component.search(); 5 6 const ctrl = TestBed.inject(HttpTestingController); 7 8 const req = ctrl.expectOne('https://[…]/flight?from=Paris&to=London'); 9 req.flush([{}, {}, {}]); 10 11 fixture.detectChanges(); 12 13 const cards = fixture.debugElement.queryAll(By.css('flight-card')); 14 expect(cards.length).toBe(3); 15 }); Mock Router and Store The test setup used so far only simulated the HttpCient. However, there are also Standalone APIs for mocking the router and NGRX: Testing Angular Standalone Components 46 1 import { provideRouter } from '@angular/router'; 2 import { provideLocationMocks } from '@angular/common/testing'; 3 4 import { provideMockStore } from '@ngrx/store/testing'; 5 import { provideMockActions } from '@ngrx/effects/testing'; 6 7 […] 8 9 describe('FlightSearchComponent (at router level)', () => { 10 let component: FlightSearchComponent; 11 let fixture: ComponentFixture; 12 let actions$ = new Subject(); 13 14 beforeEach(async () => { 15 await TestBed.configureTestingModule({ 16 providers: [ 17 provideHttpClient(), 18 provideHttpClientTesting(), 19 20 provideRouter([ 21 { path: 'flight-edit/:id', component: FlightEditComponent } 22 ]), 23 provideLocationMocks(), 24 25 provideMockStore({ 26 initialState: { 27 [BOOKING_FEATURE_KEY]: { 28 flights: [{ id:1 }, { id:2 }, { id:3 }], 29 }, 30 }, 31 }), 32 33 provideMockActions(() => actions$), 34 ], 35 imports: [FlightSearchComponent], 36 }).compileComponents(); 37 38 fixture = TestBed.createComponent(FlightSearchComponent); 39 component = fixture.componentInstance; 40 fixture.detectChanges(); 41 }); 42 43 […] Testing Angular Standalone Components 47 44 }); As with testing the HttpClient, the test first sets up the router in the usual way. Then, it uses provideLocationMocks to override a couple of internally used services, namely Location and LocationStrategy. This procedure allows the route change to be simulated in the test cases. The MockStore which also ships with NGRX is used instead of the traditional one. It allows the entire content of the store to be freely defined. This is done either by calling provideMockStore or via its setState method. Also, provideMockActions gives us the ability to swap out the actions$ observable that NGRX effects often rely on. A test case using this setup could look like as follows: 1 it('routes to flight-card', fakeAsync(() => { 2 3 const link = fixture.debugElement.query(By.css('a[class*=btn-default ]')) 4 link.nativeElement.click(); 5 6 flush(); 7 fixture.detectChanges(); 8 9 const location = TestBed.inject(Location); 10 expect(location.path()).toBe('/flight-edit/1;showDetails=false') 11 12 })); This test assumes that the FlightSearchComponent displays one link per flight in the (mock)store. It simulates a click on the first link and checks whether the application then switches to the expected route. In order for Angular to process the simulated click and trigger the route change, the change detection must be running. Unfortunately, this is not automatically the case with tests. Instead, it is to be triggered with the detectChanges method when required. The operations involved are asynchronous. Hence, fakeAsync is used so that the we don’t need to burdened ourselves with this. It allows pending micro-tasks to be processed synchronously using flush.## Testing Effects The MockStore does not trigger reducers or effects. The former are just functions and can be tested in a straight forward way. Replacing action$ is a good way to test effects. The test setup in the last section has already taken care of that. A test based on this could now use the observable action$ to send an action to which the tested effect reacts: Testing Angular Standalone Components 48 1 it('load flights', () => { 2 const effects = TestBed.inject(BookingEffects); 3 let flights: Flight[] = []; 4 5 effects.loadFlights$.subscribe(action => { 6 flights = action.flights; // Action returned from Effect 7 }); 8 9 actions$.next(loadFlights({ from: 'Paris', to: 'London' })); 10 // Action sent to store to invoke Effect 11 12 const ctrl = TestBed.inject(HttpTestingController); 13 const req = ctrl.expectOne('https://[…]/flight?from=Paris&to=London'); 14 req.flush([{}, {}, {}]); 15 16 expect(flights.length).toBe(3); 17 }); In the case under consideration, the effect triggers an HTTP call answered by the HttpTestingController. The response contains three flights, represented by three empty objects for the sake of simplicity. Finally, the test checks whether the effect provided these flights via the outbound action. Conclusion More and more libraries offer Standalone APIs for mocking dependencies. These either provide a mock implementation or at least overwrite services in the actual implementation to increase testability. The TestingModule is still used to provide the test setup. Unlike before, however, it now imports the standalone components, directives, and pipes to be tested. Their classic counterparts, on the other hand, were declared. In addition, the TestingModule now includes providers setup by Standalone APIs. Patterns for Custom Standalone APIs in Angular Together with Standalone Components, the Angular team introduced Standalone APIs. They allow for setting up libraries in a more lightweight way. Examples of libraries currently providing Standalone APIs are the HttpClient and the Router. Also, NGRX is an early adopter of this idea. In this chapter, I present several patterns for writing custom Standalone APIs inferred from the before mentioned libraries. For each pattern, the following aspects are discussed: intentions behind the pattern, description, example implementation, examples for occurrences in the mentioned libraries, and variations for implementation details. Most of these patterns are especially interesting for library authors. They have the potential to improve the DX for the library’s consumers. On the other side, most of them might be overkill for applications. Big thanks to Angular’s Alex Rickabaugh²³ for proofreading and providing feedback! Source code used in examples²⁴ Case Study for Patterns For presenting the inferred patterns, a simple logger library is used. This library is as simple as possible but as complex as needed to demonstrate the implementation of the patterns: Each log message has a LogLevel, defined by an enum: 1 export enum LogLevel { 2 DEBUG = 0, 3 INFO = 1, 4 ERROR = 2, 5 } For the sake of simplicity, we restrict our Logger library to just three log levels. An abstract LoggerConfig defines the possible configuration options: ²³https://twitter.com/synalx ²⁴https://github.com/manfredsteyer/standalone-example-cli.git Patterns for Custom Standalone APIs in Angular 50 1 export abstract class LoggerConfig { 2 abstract level: LogLevel; 3 abstract formatter: Type; 4 abstract appenders: Type[]; 5 } It’s an abstract class on purpose, as interfaces cannot be used as tokens for DI. A constant of this class type defines the default values for the configuration options: 1 export const defaultConfig: LoggerConfig = { 2 level: LogLevel.DEBUG, 3 formatter: DefaultLogFormatter, 4 appenders: [DefaultLogAppender], 5 }; The LogFormatter is used for formatting log messages before they are published via a LogAppender: 1 export abstract class LogFormatter { 2 abstract format(level: LogLevel, category: string, msg: string): string; 3 } Like the LoggerConfiguration, the LogFormatter is an abstract class used as a token. The consumer of the logger lib can adjust the formatting by providing its own implementation. As an alternative, they can go with a default implementation provided by the lib: 1 @Injectable() 2 export class DefaultLogFormatter implements LogFormatter { 3 format(level: LogLevel, category: string, msg: string): string { 4 const levelString = LogLevel[level].padEnd(5); 5 return `[${levelString}] ${category.toUpperCase()} ${msg}`; 6 } 7 } The LogAppender is another exchangeable concept responsible for appending the log message to a log: 1 export abstract class LogAppender { 2 abstract append(level: LogLevel, category: string, msg: string): void; 3 } The default implementation writes the message to the console: Patterns for Custom Standalone APIs in Angular 51 1 @Injectable() 2 export class DefaultLogAppender implements LogAppender { 3 append(level: LogLevel, category: string, msg: string): void { 4 console.log(category + ' ' + msg); 5 } 6 } While there can only be one LogFormatter, the library supports several LogAppenders. For instance, a first LogAppender could write the message to the console while a second one could also send it to the server. To make this possible, the individual LogAppenders are registered via multi providers. Hence, the Injector returns all of them within an array. As an array cannot be used as a DI token, the example uses an InjectionToken instead: 1 export const LOG_APPENDERS = new InjectionToken("LOG_APPENDERS"); The LoggserService itself receives the LoggerConfig, the LogFormatter, and an array with LogAppenders via DI and allows to log messages for several LogLevels: 1 @Injectable() 2 export class LoggerService { 3 private config = inject(LoggerConfig); 4 private formatter = inject(LogFormatter); 5 private appenders = inject(LOG_APPENDERS); 6 7 log(level: LogLevel, category: string, msg: string): void { 8 if (level < this.config.level) { 9 return; 10 } 11 const formatted = this.formatter.format(level, category, msg); 12 for (const a of this.appenders) { 13 a.append(level, category, formatted); 14 } 15 } 16 17 error(category: string, msg: string): void { 18 this.log(LogLevel.ERROR, category, msg); 19 } 20 21 info(category: string, msg: string): void { 22 this.log(LogLevel.INFO, category, msg); 23 } Patterns for Custom Standalone APIs in Angular 52 24 25 debug(category: string, msg: string): void { 26 this.log(LogLevel.DEBUG, category, msg); 27 } 28 } The Golden Rule Before I start with presenting the inferred patterns, I want to stress out what I call the golden rule for providing services: Whenever possible, use @Injectable({providedIn: 'root'})! Especially in application code but in several situations in libraries, this is what you want to have: It’s easy, tree-shakable, and even works with lazy loading. The latter aspect is less a merit of Angular than the underlying bundler: Everything that is just needed in a lazy bundle is put there. Pattern: Provider Factory Intentions Providing services for a reusable lib Configuring a reusable lib Exchanging defined implementation details Description A Provider Factory is a function returning an array with providers for a given library. This Array is cross-casted into Angular’s EnvironmentProviders type to make sure the providers can only be used in an environment scope – first and foremost, the root scope and scopes introduced with lazy routing configurations. Angular and NGRX place such functions in files called provider.ts. Example The following Provider Function provideLogger takes a partial LoggerConfiguration and uses it to create some providers: Patterns for Custom Standalone APIs in Angular 53 1 export function provideLogger( 2 config: Partial 3 ): EnvironmentProviders { 4 // using default values for missing properties 5 const merged = {...defaultConfig,...config }; 6 7 return makeEnvironmentProviders([ 8 { 9 provide: LoggerConfig, 10 useValue: merged, 11 }, 12 { 13 provide: LogFormatter, 14 useClass: merged.formatter, 15 }, 16 merged.appenders.map((a) => ({ 17 provide: LOG_APPENDERS, 18 useClass: a, 19 multi: true, 20 })), 21 ]); 22 } Missing configuration values are taken from the default configuration. Angular’s makeEnvironmentProviders wraps the Provider array in an instance of EnvironmentProviders. This function allows the consuming application to setup the logger during bootstrapping like other libraries, e. g. the HttpClient or the Router: 1 bootstrapApplication(AppComponent, { 2 providers: [ 3 4 provideHttpClient(), 5 6 provideRouter(APP_ROUTES), 7 8 [...] 9 10 // Setting up the Logger: 11 provideLogger(loggerConfig), 12 ] 13 } Patterns for Custom Standalone APIs in Angular 54 Occurrences and Variations This is a usual pattern used in all examined libraries The Provider Factories for the Router and HttpClient have a second optional parameter that takes additional features (see Pattern Feature, below). Instead of passing in the concrete service implementation, e. g. LogFormatter, NGRX allows taking either a token or the concrete object for reducers. The HttpClient takes an array with functional interceptors via a with function (see Pattern Feature, below). These functions are also registered as services. Pattern: Feature Intentions Activating and configuring optional features Making these features tree-shakable Providing the underlying services via the current environment scope Description The Provider Factory takes an optional array with a feature object. Each feature object has an identifier called kind and a providers array. The kind property allows for validating the combination of passed features. For instance, there might be mutually exclusive features like configuring XSRF token handling and disabling XSRF token handling for the HttpClient. Example Our example uses a color feature that allows for displaying messages of different LoggerLevels in different colors: For categorizing features, an enum is used: 1 export enum LoggerFeatureKind { 2 COLOR, 3 OTHER_FEATURE, 4 ADDITIONAL_FEATURE 5 } Each feature is represented by an object of LoggerFeature: Patterns for Custom Standalone APIs in Angular 55 1 export interface LoggerFeature { 2 kind: LoggerFeatureKind; 3 providers: Provider[]; 4 } For providing the color feature, a factory function following the naming pattern withFeature is introduced: 1 export function withColor(config?: Partial): LoggerFeature { 2 const internal = {...defaultColorConfig,...config }; 3 4 return { 5 kind: LoggerFeatureKind.COLOR, 6 providers: [ 7 { 8 provide: ColorConfig, 9 useValue: internal, 10 }, 11 { 12 provide: ColorService, 13 useClass: DefaultColorService, 14 }, 15 ], 16 }; 17 } The Provider Factory takes several features via an optional second parameter defined as a rest array: 1 export function provideLogger( 2 config: Partial, 3...features: LoggerFeature[] 4 ): EnvironmentProviders { 5 const merged = {...defaultConfig,...config }; 6 7 // Inspecting passed features 8 const colorFeatures = 9 features?.filter((f) => f.kind === LoggerFeatureKind.COLOR)?.length ?? 0; 10 11 // Validating passed features 12 if (colorFeatures > 1) { 13 throw new Error("Only one color feature allowed for logger!"); 14 } Patterns for Custom Standalone APIs in Angular 56 15 16 return makeEnvironmentProviders([ 17 { 18 provide: LoggerConfig, 19 useValue: merged, 20 }, 21 { 22 provide: LogFormatter, 23 useClass: merged.formatter, 24 }, 25 merged.appenders.map((a) => ({ 26 provide: LOG_APPENDERS, 27 useClass: a, 28 multi: true, 29 })), 30 31 // Providing services for the features 32 features?.map((f) => f.providers), 33 ]); 34 } The kind property of the feature is used to examine and validate the passed features. If everything is fine, the providers found in the feature are put into the returned EnvironmentProviders object. The DefaultLogAppender gets hold of the ColorService provided by the color feature via dependency injection: 1 export class DefaultLogAppender implements LogAppender { 2 colorService = inject(ColorService, { optional: true }); 3 4 append(level: LogLevel, category: string, msg: string): void { 5 if (this.colorService) { 6 msg = this.colorService.apply(level, msg); 7 } 8 console.log(msg); 9 } 10 } As features are optional, the DefaultLogAppender passes optional: true to inject. Otherwise, we would get an exception if the feature is not applied. Also, the DefaultLogAppender needs to check for null values. Patterns for Custom Standalone APIs in Angular 57 Occurrences and Variations The Router uses it, e. g. for configuring preloading or for activating debug tracing. The HttpClient uses it, e. g. for providing interceptors, configuring JSONP, and configuring/ disabling the XSRF token handling Both, the Router and the HttpClient, combine the possible features to a union type (e.g. export type AllowedFeatures = ThisFeature | ThatFeature). This helps IDEs to propose built-in features. Some implementations inject the current Injector and use it to find out which features have been configured. This is an imperative alternative to using optional: true. Angular’s feature implementations prefix the properties kind and providers with and hence declare them as internal properties. Pattern: Configuration Provider Factory Intentions Configuring existing services Providing additional services and registering them with existing services Extending the behavior of a service from within a nested environment scope Description Configuration Provider Factories extend the behavior of an existing service. They may provide additional services and use an ENVIRONMENT_INITIALIZER to get hold of instances of both the provided services as well as the existing services to extend. Example Let’s assume an extended version of our LoggerService that allows for defining an additional LogAppender for each log category: Patterns for Custom Standalone APIs in Angular 58 1 @Injectable() 2 export class LoggerService { 3 4 private appenders = inject(LOG_APPENDERS); 5 private formatter = inject(LogFormatter); 6 private config = inject(LoggerConfig); 7 [...] 8 9 // Additional LogAppender per log category 10 readonly categories: Record = {}; 11 12 log(level: LogLevel, category: string, msg: string): void { 13 14 if (level < this.config.level) { 15 return; 16 } 17 18 const formatted = this.formatter.format(level, category, msg); 19 20 // Lookup appender for this very category and use 21 // it, if there is one: 22 const catAppender = this.categories[category]; 23 24 if (catAppender) { 25 catAppender.append(level, category, formatted); 26 } 27 28 // Also, use default appenders: 29 for (const a of this.appenders) { 30 a.append(level, category, formatted); 31 } 32 33 } 34 35 [...] 36 } To configurate a LogAppender for a category, we can introduce another Provider Factory: Patterns for Custom Standalone APIs in Angular 59 1 export function provideCategory( 2 category: string, 3 appender: Type 4 ): EnvironmentProviders { 5 // Internal/ Local token for registering the service 6 // and retrieving the resolved service instance 7 // immediately after. 8 const appenderToken = new InjectionToken("APPENDER_" + category); 9 10 return makeEnvironmentProviders([ 11 { 12 provide: appenderToken, 13 useClass: appender, 14 }, 15 { 16 provide: ENVIRONMENT_INITIALIZER, 17 multi: true, 18 useValue: () => { 19 const appender = inject(appenderToken); 20 const logger = inject(LoggerService); 21 22 logger.categories[category] = appender; 23 }, 24 }, 25 ]); 26 } This factory creates a provider for the LogAppender class. However, we don’t need the class but rather an instance of it. Also, we need the Injector to resolve this instance’s dependenc

Use Quizgecko on...
Browser
Browser