LiveLoveApp logo

Angular 14 Standalone Components

Published by Brian Love on
Angular 14 Standalone Components
Angular 14 Standalone Components

Angular version 14 is a feature-packed release that brings new APIs, functionality, and a developer experience. Angular version 14 is arguably the biggest release since version 9 when Angular's newest compilation and rendering engine (called Ivy) was released.

This article is the first in a series that will cover the key features and takeaways that I believe angular developers and engineering managers should know about Angular version 14. First, we'll start with the hottest new topic called standalone components. Then, we'll dive into typed forms, what they are, and what this means for you and your organization. We'll talk about version dependency updates, improvements to the Angular Language Service, new configurations to improve testing at runtime, and a small compiler update.

Let's dive into Angular's new developer preview of standalone components!

Angular 14 Playground on Stackblitz

Real quick - before we dive into the details - I want to mention that I have an Angular 14 Playground for you on Stackblitz. Much of the example code below is referenced from this small project. Check it out and feel free to fork it!

Full code on Stackblitz

What is a standalone component?

Prior to version 14, all components had to be declared as part of the declarations array in an NgModule. NgModules are a critical building piece to solve architectural challenges in Angular, namely importing other modules in your codebase or importing other modules that are published as part of a library (using the Angular Package Format. NgModules also provide a mechanism for wiring up and configuring dependency injection. We'll discuss both of these in more detail below.

Standalone components enable Angular developers to build applications without using the NgModule based approach.

The immediate question is what about applications built today with modules? Will modules be supported in the future?

The answer is a resounding yes. Angular applications, and libraries, that are built with modules will continue to be supported. Flatly stated, modules are not going anywhere.

Further, Angular's new standalone component architecture is fully compatible with the existing module-based architecture. You can continue to use modules where necessary and/or preferred, and you can start using standalone components alongside them. Based on your team and organization's architectural style, you can start adopting standalone components, or you can continue to build Angular applications using modules as you have been doing for the past 6 years. This continues on the Angular Team's promise to not leave anyone behind with breaking changes.

Will standalone components replace modules as the de facto style?

At the time of this writing, as Angular version 14 is just now being released, the answer to this question is unknown. This will likely depend on community adoption and tooling. Further, the current documentation, getting started journey, and style guide do not teach standalone components over the module-based approach.

Why are standalone components in “developer preview”?

Standalone components are being released in version 14 as a developer preview. This means that the LTS policy for the APIs does not apply to standalone components. This is a good thing! As the community adopts this new architectural pattern we will all learn from each other what works well, what is cumbersome, and, potentially, what are the edge cases where this architectural pattern breaks. This learning enables the framework to innovate at a fast pace. It also means that the APIs, while public, may change in future minor releases.

Getting Started with standalone Components

To use standalone components, Angular has introduced a new standalone property in the component metadata. The property is false by default.

Here is a quick example of a standalone component:

import { Component, Input } from '@angular/core';

@Component({
  standalone: true,
  selector: 'app-root',
  template: `
    <ng-content></ng-content>, {{ name }}.
  `,
  styles: [``],
})
export class NameComponent {
  @Input() name = '';
}

The code example above is like any Angular component except that we have set the standalone property to true. This instructs the compiler to treat this component as standalone, and further, this prevents us from including the component in the declarations array of a module.

Standalone components must declare their own dependencies including child standalone components. For example, to use the <app-name> component in another standalone component, I must import the component:

@Component({
  selector: 'my-app',
  standalone: true,
  template: `
    <app-name [name]="name">Hi</app-name>
  `,
  imports: [CommonModule, NameComponent],
})
export class AppComponent {
  name = 'Brian Love';
}

In the code example above note that I have included our NameComponent in the imports array in the component metadata. This instructs the compiler that this component is a dependency of the AppComponent, which is also a standalone component.

What about existing NgModule uses?

As stated previously, standalone components are fully compatible with existing codebases that use the NgModule pattern. If a standalone component uses a directive, component, or pipe that is exported from a module then we include the module in the imports array in the standalone component metadata to import the module. All publicly exported members of the module are now available for use in the standalone component.

Let's expand our current example application to use Angular Material. To do so, we'll need to import the necessary modules:

@Component({
  selector: 'app-root',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [
    CommonModule,
    MatIconModule,
    MatListModule,
    MatSidenavModule,
    MatToolbarModule,
    RouterModule,
  ],
  template: `
    <mat-toolbar class="toolbar">
      <button mat-icon-button aria-label="Toggle menu" (click)="toggleMatSidenav()">
        <mat-icon>menu</mat-icon>
      </button>
      <span>Angular v14 Playground</span>
    </mat-toolbar>
    <mat-sidenav-container class="container">
      <mat-sidenav [(opened)]="matSidenavOpened" fixedTopGap="64" fixedInViewport>
        <mat-nav-list>
          <a mat-list-item routerLink="/">Home</a>
          <a mat-list-item routerLink="/about">About</a>
          <a mat-list-item href="https://liveloveapp.com" target="_blank">Learn More about LiveLoveApp</a>
        </mat-nav-list>
      </mat-sidenav>
      <mat-sidenav-content class="content">
        <main>
          <router-outlet></router-outlet>
        </main>
      </mat-sidenav-content>
    </mat-sidenav-container>
  `
})
export class AppComponent {
  matSidenavOpened = false;
  name = 'Brian Love';

  toggleMatSidenav(): void {
    this.matSidenavOpened = !this.matSidenavOpened;
  }
}

Let's review the code above:

  • First, you'll note that the AppComponent has the standalone property set to true in the component metadata.
  • I went ahead and update the change detection strategy, really just to test out how this works with standalone components, and thankfully, it works as expected.
  • Note the imports array. I've imported the necessary material modules that I need for the component. I've also imported the RouterModule since my component's template includes the <router-outlet> custom element.
  • For the sake of brevity, I skipped the styles (but you can check out the full Angular v14 Playground demo on Stackblitz).

Dependency Injection with standalone Components

Before we dive into a few of the important updates in Angular v14 to support standalone components, let me reiterate a few things.

First, the developer experience for module-based Angular applications using dependency injection has no breaking changes, and for the most part, has not changed. You can continue to use the injector, injection tokens, providers, and the @Injectable() decorator just as you have prior to Angular version 14.

Second, the dependency injector hierarchy is still very similar, with a few exceptions that we'll cover shortly.

Module type injectors are available using the providers array within the NgModule metadata as well as by using the providedIn decorator. Specifying the root value for the providedIn decorator will register the class at the root level that is available throughout your Angular application.

Here is a quick example of module type injectors that you are likely familiar with:

// Module type injector using NgModule metadata
@NgModule({
  providers: [
    UserService,
    {
      provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
      useValue: { appearance: 'outline' },
    },
  ]
})
export class AppModule {}

// Module type injector using providedIn property
@Injectable({
  providedIn: 'root'
})
export class UserService {}

Node type injectors enable us to limit the provider scope through the use of the providers array for a directive or component. One common use case for limit provider scope is when you are using NgRx's Component Store:

// node type injector
@Component({
  providers: [GridComponentStore]
})
export class GridComponent {}

Now, let's learn some of the new terminology and features introduced in Angular version 14.

First, Angular 14 adds a new viewProviders property to the @Component() metadata that enables us to further limit the provider scope to children of the existing component.

// node type injector usin `viewProviders`
@Component({
  selector: 'app-name',
  template: `
    Hello, {{ user$ | async | greet }}
  `
})
export class NameComponent {
  user$ = this.userService.user$;

  constructor(private readonly userService: UserService) {}
}

@Component({
  viewProviders: [UserService],
  template: `
    <app-name></app-name>
  `
})
export class ToolbarComponent {}

Next, Angular 14 introduces a new term called “Environment Injectors”. Environment injectors cover the following scenarios:

  • Module type injectors. As discussed above, this includes providers declared in a module as well as those that use the providedIn property for the @Injectable() metadata.
  • Providers that are declared when the application is bootstrapped.
  • Providers that are declared within the providers array for a Route.

Let's look at an example of declaring providers when an application is bootstrapped. Common use cases include providing the BrowserAnimationsModule, registering root-level routes using the RouterModule.forRoot() static method, and registering NgRx's global store using the StoreModule.forRoot() static method.

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom([
      BrowserAnimationsModule,
      RouterModule.forRoot(routes)
    ]),
  ],
});

In the example above we are also introducing the importProvidersFrom() function. This utility function collects all providers from one or more sources that are either a standalone component or an NgModule.

Also introduced in Angular 14, we can declare an array of providers within a Route configuration that will create an Environment Injector at the route level. This enables the providers to be used within all components within the route, and all child routes.

const routes = [
  {
    path: 'users',
    loadChildren: () =>
      import('./users.module').then(({ UsersModule }) => UsersModule),
    providers: [
			UserService,
	    {
	      provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
	      useValue: { appearance: 'outline' },
	    },
    ]
  },
  { path: '**', redirectTo: '' },
] as Routes;

Finally, Angular 14 introduces an additional injector type that is termed a “Standalone Injector”. No, the poor injector is not standing alone on the playground without any friends. The Standalone Injector is a child of the root environment injector and is responsible for isolating all providers for standalone components.

Routing with standalone Components

Angular 14 introduces an expanded API for routing with standalone components, including a feature that enables us to very easily lazy load a standalone component. If you are familiar with the router's loadChildren configuration property that enables lazy-loading modules, then you will be very comfortable using the new loadComponent property.

const routes = [
  {
    path: 'about',
    loadComponent: () =>
      import('./about.component').then(({ AboutComponent }) => AboutComponent),
  }
] as Routes;

In the code example above we are lazy loading a single standalone component at runtime with a simple configuration within the route.

Angular 14 also introduces a new feature that enables us to lazy load routes without the need to wrap them up in an NgModule using the RouterModule.forChild() static method.

const routes = [
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/routes').then(({ routes }) => routes),
  }
] as Routes;

Note, that in order to use this new feature, all routes must use standalone components. This feature is not compatible with existing non-standalone components defined within the routes.

Conclusion

In conclusion, Angular version 14 shipped a new developer preview of the standalone components API. This API enables Angular developers to build applications without the use of the NgModule architecture. The primary goals of standalone components are to simplify the API, improve developer ergonomics and velocity, and to enable future innovation in the Angular ecosystem. Standalone components do introduce some changes to the dependency injection system and the routing story. Finally, we should note that this new feature is backward compatible with existing Angular code that uses the NgModule architecture, and that this is a developer preview - meaning that the API is not finalized and could have breaking changes in the future.

Design, Develop and Deliver Absolute Joy
Schedule a Call