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 {}
The Component decorator now accepts two new options:
standalone, which indicates whether a component is standalone or notimports: available only whenstandaloneis 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 { }
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();
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
providersoption, used to register a provider - the
importModuleWithProvidersfunction, 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();
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)
}
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 }
]
]
Careful: all components must be standalone components.
{
path: 'user', loadChildren: () =>
import('./feature/user.route').then(r => r.USER_ROUTES)
}
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();
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 {}
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.
