Introduction

Angular 14 is nearly here, and the community is eagerly awaiting this release because of the features it introduces:

  • standalone components
  • typed forms
  • composition patterns

Each of these changes will genuinely reshape how we build our applications.

How do you use these features? What effects do they have?

This article is based on Angular 14 release candidate 1. The APIs described may still change.

Standalone Components

Up until now, Angular applications were organized around the concept of modules. Those modules held the declarations for components, directives, pipes, and providers.
In short, modules were there to manage dependencies.

With Angular 14, the team decided, through the RFCs found here and there, to streamline this model and make the component the central element of Angular applications (similar to what Vue or React do).

To make this possible, Angular has introduced the notion of standalone components

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  stylesUrl: './app.component.css',
  standalone: true,
  imports: [CommonModule]
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

The Component decorator now accepts two new options:

  • standalone, which indicates whether a component is standalone or not
  • imports: available only when standalone is set to true. This option lets you bring in other standalone components or modules.

But why do we need the imports option to import modules?

The truth is that when a component is standalone, Angular implicitly creates a module behind the scenes (often referred to as a virtual module).

Marking a component as standalone is the equivalent of writing

@Module({
   declatations: [AppComponent],
   exports: [AppComponent]
})
export class AppComponent { }
Enter fullscreen mode Exit fullscreen mode

So it makes sense to import the modules your standalone component depends on, preserving the current system that already exists.

This feature brings changes to several areas:

  • routing
  • modules
  • application bootstrapping

The Angular model now leans more toward the component than the module. In earlier versions, you had to bootstrap from a module; that's no longer true. Angular lets you bootstrap directly from a component, provided it is standalone

import { bootstrapApplication } from '@angular/platform-browser';

bootstrapApplication(AppComponent, { providers: [] }).then().catch();
Enter fullscreen mode Exit fullscreen mode

But if there's no more module, how do we import something like HttpClientModule or BrowserAnimationModule, and especially how do we declare our routing?

Angular believes this problem can be solved through providers. In fact, that's mostly what the modules just mentioned expose.

To tackle this, Angular changes these APIs in two ways:

  • the providers option, used to register a provider
  • the importModuleWithProviders function, which imports the providers a module exposes.

So if you want to bring in Angular's HTTP client for the whole application, you just write:

bootstrapApplication(AppComponent, { providers: [
  importModuleWithProviders(HttpClientModule),
] }).then().catch();
Enter fullscreen mode Exit fullscreen mode

Note: In the future, Angular will provide a new withHttpClient() function that does the same job as above.

On the Routing side, standalone components have opened up a wide range of possibilities:

  • lazy-loading a standalone component
  • lazy-loading a group of standalone components via a route configuration file

Lazy-loading a standalone component

There's a new option available when declaring routes: loadComponent. It follows exactly the same syntax as loadChildren, except that it imports a standalone component.

{
  path: 'user', loadComponent: () => 
    import('./feature/user.component).then(cp => cp.UserComponent)
}
Enter fullscreen mode Exit fullscreen mode

Lazy-loading a set of standalone components

Without modules, the question of how to handle child navigation comes up.

Angular has extended the loadChildren API so it can load a route configuration file in addition to a module.

export const USER_ROUTES: Routes = [
 path: '', component: UserComponent, children: [
    { path: 'admin', component: UserAdminComponent },
    { path: 'sub-admin', component: UserSubAdminComponent }
  ]
]
Enter fullscreen mode Exit fullscreen mode

Careful: all components must be standalone components.

{
  path: 'user', loadChildren: () => 
    import('./feature/user.route').then(r => r.USER_ROUTES)
}
Enter fullscreen mode Exit fullscreen mode

Loading the routing configuration into the application works just like it does for HttpClientModule.

bootstrapApplication(AppComponent, { providers: [
  importModuleWithProviders(HttpClientModule),
  importModuleWithProviders(RouterModule.forRoot(APP_ROUTES))
] }).then().catch();
Enter fullscreen mode Exit fullscreen mode

This GitHub link shows an example of how standalone components work.

This concept applies not just to components, but also to directives and pipes.

@Directive({
  selector: '[focus]'
  standalone: true
})
export class FocusDirective {}

@Pipe({
  name: 'na',
  standalone: true
})
export class NaPipe implements PipeTransform {}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Standalone components unlock new ways to make our applications and libraries more scalable.

The atomic architecture championed by Angular, and the use of modules, remain relevant — the only difference is less boilerplate (two files instead of three for bootstrapping, for example).

In this new version, components, directives, and pipes can be used 'out of the box.' There's no need to declare them in a module beforehand.