7

I tried the Angular Standalone migration, and here is the result

 2 years ago
source link: https://timdeschryver.dev/blog/i-tried-the-angular-standalone-migration-and-here-is-the-result#migration-examples
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Tim Deschryver

BLOG RSS
🔗 I tried the Angular Standalone migration, and here is the result

I tried the Angular Standalone migration, and here is the result

I tried the Angular Standalone migration, and here is the result
Published February 06, 2023
profile
Tim Deschryver

Last week Minko Gechev tweeted about an Angular schematic to automate the migration from @NgModules to the standalone API. Of course, I had to try out this migration myself.

To test the migration I created a small Angular application, which I will use as a starting point for the migration. While the application is small, it contains a little bit of everything, child components, eager and lazy loaded modules, a pipe, a directive, a couple of tests, and a service.

The schematic is available starting from Angular version 15.2.0-next.2. To update to this version or a later version, run the following command:

npx ng update --next
npx ng update @angular/core --next

You can take a look at the before branch on GitHub if you're interested in the code from the example project.

To run the migration open a new CLI terminal at the root of the Angular project and run the @angular/core:standalone schematic:

npx ng generate @angular/core:standalone

This gives you three options:

To complete the migration, you need to run all three options. Instead of migrating your whole codebase at once, you can also run the schematic on specific directories.

The first time I ran the schematic I tried to keep the application running and the tests green. For this, I had to manually update some parts of the code and tests (see the steps below). But while running the next options, I noticed that the schematic was also fixing some of the issues I had to fix manually. That's why I decided to run the schematic from the start again, but this time I ran the schematics after each other without updating the code and tests. Looking back at it, I think the latter is the way to go, although it seems not to be recommended in the Angular docs.

The schematic only migrates the code from NgModules to the new standalone API syntax. But, lately Angular also added a bunch of new functional APIs. For the completeness of this migration, I also manually migrated some features that are not covered by the schematic to their new equivalent functional API version.

If you're not interested in the details, you can take a look at the migrated version on the main branch (with manual changes between migration steps) or on the after branch (all migrations at once, and manual changes afterward).

Why you should migrate link

I think you should migrate to the standalone components because it has a few benefits.

The foremost is that Angular has a smoother learning curve for new developers. For new and experienced developers, a big advantage is a simplified codebase, which is easier to understand and maintain.

It also has a few performance benefits, e.g. you can lazy load a component because it defines its own dependencies explicitly.

Another benefit is that your tests require less setup code. In most cases, you only need to import the component you want to test and mock the external dependencies e.g. an HTTP service.

And who knows, perhaps somewhere in the future that Angular can automagically import the dependencies for you, and is step this step just an intermediate step to make that possible. But for now, you need to do it manually.

From the docs docs:

Standalone components provide a simplified way to build Angular applications. Standalone components, directives, and pipes aim to streamline the authoring experience by reducing the need for NgModules. Existing applications can optionally and incrementally adopt the new standalone style without any breaking changes.

1. Convert all components, directives, and pipes to standalone link

Commit: d32df876bebc4f1824589bca14799cc27d6ff602:

Command link

npx ng generate @angular/core:standalone
Convert all components, directives, and pipes to standalone

Results link

  • Components, directives, and pipes are migrated to the standalone version
  • Dependencies are added to the standalone versions
  • NgModules are updated, e.g. components are moved from the declarations to the imports

Manual changes link

  • A child component referenced in a Route was not migrated. This was fixed in the next migration while running all schematics at once.
  • Update TestBed: move standalone components/directives/pipes from declarations to imports
  • Declarables are moved from the declarations to the imports of an NgModule

Notes link

  • AppComponent is not migrated
  • It also imports an internal ɵInternalFormsSharedModule module together with the FormsModule or ReactiveFormsModule

Migration Examples link

Components are migrated to standalone components:

  • standalone is set to true
  • dependencies are added to imports


lazy-child.component.ts

import { Component } from '@angular/core';
import { JsonPlaceholderService } from '../services/json-placeholder.service';
+ import { SensitivePipe } from '../../shared-module/pipes/sensitive.pipe';
+ import { AsyncPipe, JsonPipe } from '@angular/common';
+ import { ɵInternalFormsSharedModule, FormsModule } from '@angular/forms';
+ import { MatInputModule } from '@angular/material/input';
+ import { MatFormFieldModule } from '@angular/material/form-field';
+ import { HighlightDirective } from '../../highlight-directive/highlight.directive';
@Component({
selector: 'app-lazy-child',
template: `
<div class="container">
<p><span appHighlight>lazy-child</span> works!</p>
<p>{{ 'eager-child works!' | sensitive }}</p>
<mat-form-field>
<mat-label>eager-child</mat-label>
<input matInput type="text" name="name" [(ngModel)]="form.name" />
</mat-form-field>
<pre>{{ todos$ | async | json }}</pre>
</div>
+ standalone: true,
+ imports: [HighlightDirective, MatFormFieldModule, MatInputModule, ɵInternalFormsSharedModule, FormsModule, AsyncPipe, JsonPipe, SensitivePipe]
export class LazyChildComponent {
form = {
name: '',
todos$ = this.placeholderService.getTodos();
constructor(private placeholderService: JsonPlaceholderService) {}

NgModules are updated by moving the declarations to the imports:



shared.module.ts

import { SensitivePipe } from './pipes/sensitive.pipe';
@NgModule({
- declarations: [SensitivePipe],
imports: [
CommonModule,
MatInputModule,
MatFormFieldModule,
imports: [
CommonModule,
MatInputModule,
MatFormFieldModule,
+ SensitivePipe,
exports: [MatInputModule, MatFormFieldModule, SensitivePipe]
export class SharedModule {}

2. Remove unnecessary NgModule classes link

Commit: c74471ae5b9627ab73ed0e163600834d4d51f85d

Command link

npx ng generate @angular/core:standalone
Remove unnecessary NgModule classes

Results link

  • Files only containing an NgModule are deleted
  • NgModuless that reference the removed NgModules are updated

Manual changes link

  • Update TestBed: remove deleted NgModules
  • Commented a child component in AppComponent. This was fixed in the next migration while running all schematics at once.

Migration Examples link

The file shared.module.ts is deleted because it only contained an NgModule, SharedModule:

NgModuless that reference the removed NgModules are updated.



lazy.module.ts

import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { LazyChildComponent } from './lazy-child/lazy-child.component';
- import { SharedModule } from '../shared-module/shared.module';
import { LazyRoutingModule } from './lazy-routing.module';
- import { HighlightModule } from '../highlight-directive/highlight.module';
@NgModule({
imports: [
CommonModule,
LazyRoutingModule,
- SharedModule,
FormsModule,
- HighlightModule,
LazyChildComponent
export class LazyModule {}

3. Bootstrap the application using standalone APIs link

Commit: 16c649d64130741ea75e4d35517ffd6b5b80cdc8

Command link

npx ng generate @angular/core:standalone
Bootstrap the application using standalone APIs

Result link

  • main.ts is updated from platformBrowserDynamic().bootstrapModule(AppModule) to bootstrapApplication(AppComponent)

Manual changes link

  • Readded the child component that was removed in the previous step. This was not needed while running all the schematics at once.

Notes link

  • AppModule still exists, but the content is commented out
  • It seems like files are imported by using the \\ separator instead of /

Migration Examples link



main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
+ import { importProvidersFrom } from '@angular/core';
+ import { AppComponent } from './app\\app.component';
+ import { provideAnimations } from '@angular/platform-browser/animations';
+ import { AuthConfigModule } from './app\\auth\\auth-config.module';
+ import { AppRoutingModule } from './app\\app-routing.module';
+ import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
+ import { AuthInterceptor } from 'angular-auth-oidc-client';
+ import { HTTP_INTERCEPTORS } from '@angular/common/http';
- platformBrowserDynamic().bootstrapModule(AppModule)
+ bootstrapApplication(AppComponent, {
+ providers: [
+ importProvidersFrom(BrowserModule, AppRoutingModule, AuthConfigModule),
+ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
+ provideAnimations()
.catch(err => console.error(err));

4. Remove unnecessary NgModule classes (for AppModule) link

Commit: c05ca76aad7717e303037e33c269602627ab9720

Command link

npx ng generate @angular/core:standalone
Remove unnecessary NgModule classes

Result link

  • Now that AppModule is not used anymore, it is deleted

5. Migrate to provideRouter link

Commit: 528661c9cef1e9f3bf5cb83ff6571c96c4ae8164

This is not an automatic migration.

Result link

We can use provideRouter() instead of RouterModule.forRoot() and RouterModule.forChild().

For more info about provideRouter see Angular Router Standalone APIs by Kevin Kreuzer.

Migration Examples link



app-routing.module.ts

import { importProvidersFrom } from '@angular/core';
import { AppComponent } from './app\\app.component';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(BrowserModule),
}).catch((err) => console.error(err));

6. Migrate to provideHttpClient link

Commit: 655217f3f528fc7db83515cfce59275043dd6183

This is not an automatic migration.

Result link

Instead of importing HttpClientModule in AppModule, and registering interceptors as providers with HTTP_INTERCEPTORS we can now use provideHttpClient().

For more info about provideHttpClient see The Refurbished HttpClient in Angular 15 – Standalone APIs and Functional Interceptors by Manfred Steyer.

Migration Examples link



main.ts

import { bootstrapApplication } from '@angular/platform-browser';
- import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
import {
AuthInterceptor,
authInterceptor,
} from 'angular-auth-oidc-client';
import { AuthConfigModule } from './app\\auth\\auth-config.module';
bootstrapApplication(AppComponent, {
providers: [
- importProvidersFrom(AuthConfigModule, HttpClientModule),
- { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
+ importProvidersFrom(AuthConfigModule),
+ provideHttpClient(withInterceptors([authInterceptor()])),
}).catch((err) => console.error(err));

7. Migrate to functional router guards link

Commit: 6b1977d24e9770871f432b0eaa0e24efd94d41fe

This is not an automatic migration.

Result link

A router guard that was implemented as a class can be refactored to a function.

For more info about functional router guards see How To Use Functional Router Guards in Angular by Dany Paredes . It's probably best to immediately migrate to the new canMatch guard, for more info see Introducing the CanMatch Router Guard In Angular by Netanel Basal.

Migration Examples link

Before:



authorized.guard.ts

import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AuthorizationGuard implements CanActivate {
constructor(
private oidcSecurityService: OidcSecurityService,
private router: Router
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> {
return this.oidcSecurityService.isAuthenticated$.pipe(
map(({ isAuthenticated }) => {
// allow navigation if authenticated
if (isAuthenticated) {
return true;
// redirect if not authenticated
return this.router.parseUrl('');

After:



authorized.guard.ts

import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { map } from 'rxjs';
export const authenticatedGuard = () => {
const router = inject(Router);
const securityService = inject(OidcSecurityService);
return securityService.isAuthenticated$.pipe(
map(({ isAuthenticated }) => {
// allow navigation if authenticated
if (isAuthenticated) {
return true;
// redirect if not authenticated
return router.parseUrl('');

8. Update tests, only import standalone components link

Commit: 7e04027511b8ece03522bb3e52e87775e4f7dd8a

This is not an automatic migration.

Result link

Because a component now contains all its dependencies, we can refactor the test cases. The test becomes simpler because we are not required to import all the dependencies anymore. Instead, we can import the component itself.

Migration Examples link



app.component.spec.ts

await TestBed.configureTestingModule({
imports: [
- MatFormFieldModule,
- MatInputModule,
- ReactiveFormsModule,
EagerChildComponent,
- SensitivePipe,
}).compileComponents();
const fixture = TestBed.createComponent(LazyChildComponent);
Support me

I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.

5a4c6b hosted on Azure | Last update: 2023-02-06T20:36:48.792Z.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK