How to turn an Angular app into standalone - Part II
Tags: webdev, angular, typescript, tutorial
Published: 2022-10-28 15:57:21

Today we will attempt to get rid of all `NgModule` in our simple app. To make sure we cover a few angles, we will spice it up with a `ProjectService` that is provided in root, and a `LOCALE_ID` token provided in the app root module. We will also create a simple form and use the HTTP client.

The plan is:

-   Get rid of `SharedModule`
-   Make the content route fully standalone and lazy-load it
-   Get rid of the `ProjectRoutingModule` and its dependencies
-   Bootstrap a standalone root component
-   Run with injection tokens and providers

The final project is on [StackBlitz](https://stackblitz.com/edit/angular-standalone-app-full)

Getting rid of the `SharedModule`
---------------------------------

As we covered previously; to have another shared array of components that are imported throughout the application may not be a great win. The better approach is to **import only the required component, directive or pipe, when needed**. Nevertheless, for demo purposes let's create a standalone alternative of `SharedModule`:

```ts
// core/shared.module
// the current way was to create a module and add all common components in declarations
@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [
    StarsPartialComponent,
    PagerPartialComponent,
  ],
  exports: [
    StarsPartialComponent,
    PagerPartialComponent,
    CommonModule
  ],
})
export class SharedModule {}
```

The new shared components look like this

```ts
// core/shared.const.ts
// export all shared modules is to create a const
export const SHARED_COMPONENTS = [
  // add common standalone components, turn them all to standalone
  StarsPartialComponent,
  PagerPartialComponent
] as const;
```

Lazy loading the content module
-------------------------------

We had a `Content` lazy loaded route with two routes, one of them was already standalone. First, let's make a test: turn a lazy-loaded route into standalone, without changing the components included.

```ts
// app.route.ts module
const AppRoutes: Routes = [
  // ..
  // let's turn this into standalone by importing the routes const instead
  {
    path: 'content',
    loadChildren: () =>
      // import('./routes/content.route').then((m) => m.ContentRoutingModule),
    	// import routes instead
  	  import('./routes/content.route').then((m) => m.ContentRoutes),
	},
];
@NgModule({
  imports: [RouterModule.forRoot(AppRoutes)],
  exports: [RouterModule],
})
export class AppRouteModule {}
```

The `ContentRoutes` array looks like this

```ts
// routes/content.route
export const ContentRoutes: Routes = [
  {
    // not yet turned into standalone
    path: 'showcard',
    component: ContentShowcardComponent,
  },
  {
    // standalone
    path: 'standalone',
    component: ContentStandaloneComponent,
  },
];
```

This compiles and builds just fine. But it will fail to run, so **be careful**. It would fail the unit tests, but if you do not include basic tests (the type that would fail in design time), this might slip. This is one of the shortcomings of the standalone feature.

Then let's turn all child components to standalone.

### Our standalone components with too much freedom

In the above section we turned all standalone child components into a route `const`, we can also route directly to any of these components **from anywhere in the system**. Here is an example of routing to `ContentStandaloneComponent` from the app root:

```ts
// app.route module
// ...
const AppRoutes: Routes = [
  // lazy loaded group of sub routes
  {
    path: 'content',
    loadChildren: () =>
      import('./routes/content.route').then((m) => m.ContentRoutes),
  },
  // lazy loaded component, from the same group above
  {
    path: 'lazyloadedstandalone',
	  // notice the new loadComponent function
    loadComponent: () =>
      import('./components/content/standalone.component').then(
        (m) => m.ContentStandaloneComponent
      ),
  },
];
```

This means we can load the component with two routes:

`/content/standalone`

`/lazyloadedstandalone`

With modules, we were able to do that whether in the same module or exported modules. But that was a bad idea already. Now that the restriction of modules is gone, the bad habit may go out of control. So curb your enthusiasm, or stay Module-bound.

Getting rid of a feature route module
-------------------------------------

For demo purposes let's add a `/projects/create` route that contains a partial component with a form. We'll also use an HTTP call (look inside the `ProjectService` to find it).

```ts
// the current ProjectRoutingModule with a forms route
const routes: Routes = [
  {
    path: '',
    component: ProjectListComponent,
  },
  // new path with a form and an http request
  {
    path: 'create',
    component: ProjectCreateComponent
  },
  {
    path: ':id',
    component: ProjectViewComponent,
  }
];
```

The `create` component needs `HttpClientModule`, and the form partial needs `ReactiveFormsModule`, we will import the former in the app root module and the latter in the `ProjectRoutingModule` directly. And the `ProjectService` shall be provided in `root` for now. Let's rip down the `ProjectRoutingModule` one piece at a time.

### The simple list baggage

The process of turning the components into standalone is the same, we simply need to copy over *all* dependencies into the `imports` array, here is how the Project list component now looks like:

```ts
@Component({
  templateUrl: './list.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [
    // import everything, seriously?
    ProjectCardPartialComponent,
    RouterModule,
    LetDirective,
    CommonModule,
    ...SHARED_COMPONENTS,
  ],
})
export class ProjectListComponent implements OnInit {
  // ...
}
```

The forms **partial** component looks interesting:

```ts
@Component({
  // ...
  // if this is standalone it needs only the things it uses, not the parent
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
})
export class ProjectFormPartialComponent implements OnInit {
  // ...
}
```

And the `ProjectCreateComponent` that houses it looks like this

```ts
// projects/create.component
@Component({
  templateUrl: './create.html',
  standalone: true,
  // notice we do not import the forms module here,
  imports: [ProjectFormPartialComponent],
})
// ...
```

Now the `ProjectRoutingModule` has no declarations, we'll turn the module into an exported `ProjectRoutes` `const` and drop the `NgModule`

```ts
// routes/project.route becomes simple exported const
export const ProjectRoutes: Routes = [
 //...
];

// and app.routes will lazy load this routes variable
{
  path: 'projects',
  loadChildren: () =>
    import('./routes/project.route').then((m) => m.ProjectRoutes),
},
```

The service instance need not be changed, and `HttpClientModule` still works. If we want to stop providing the `ProjectService` in `root` we can add it to the `providers` array of the route, either on the single route that uses it or the parent lazy loaded route. The difference is how shared that instance throughout the application we need it to be.

```ts
// routes/project.route becomes simple exported const
export const ProjectRoutes: Routes = [
 //...
];

// and app.routes will lazy load this routes variable
{
  path: 'projects',
  loadChildren: () =>
    import('./routes/project.route').then((m) => m.ProjectRoutes),
},
```

Since we are still using the root app module, the `HttpClientModule` can still be imported in it. So what if we want to drop the app module?

Tear down the `AppModule`
-------------------------

Today we `bootstrapModule` in the `main.ts` to bootstrap the main component. If we want to strip all of our application of modules, we can use the new function ****`bootstrapApplication`.**

```ts
// in main.ts, we bootstrap
bootstrapApplication(AppComponent);
```

In order to pass our app routes, we need to use [`importProvidersFrom`](https://angular.io/api/core/importProvidersFrom). Which is Angular's way of saying: here is a band-aid!

> Though I still cannot see how in the future we would be able to create routes without `RouterModule`

```ts
// main.ts bootstrap passing all providers from an existing NgModule
bootstrapApplication(AppComponent, {
  providers: [
    // pass the routes from existing RouterModule
    // the AppRoutes now is module-free
    importProvidersFrom(RouterModule.forRoot(AppRoutes)),
  ],
});
```

That leaves the `App.Component`, to turn it to standalone, we only need to import what it needs:

```ts
// app.component
@Component({
  selector: 'my-app',
  // ...
  standalone: true,
  // I need only the router-outlet, routerLink, and the gr-toast partial component
  imports: [RouterModule, ToastPartialComponent],
})
export class AppComponent {
// ...
}
```

Now we can do the same for `HttpClientModule`, we can provide in any route, whether in `main.ts`, in the `app.route` or in `project.route`. It is still a good idea to have it on root thought.

```ts
// locations to provide the HttpClientModule
// in main.ts
bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(RouterModule.forRoot(AppRoutes), HttpClientModule),
    // ...
  ],
});

// in app.route
export const AppRoutes: Routes = [
  {
    path: 'projects',
    loadChildren: () =>
	    import('./routes/project.route').then((m) => m.ProjectRoutes),
	    // provide httpclient here as well
	    providers: [ProjectService, importProvidersFrom(HttpClientModule)],
  },
// ...
]

// or in routes/project.route for a specific route
export const ProjectRoutes: Routes = [
  {
    path: 'create',
    component: ProjectCreateComponent,
    // all what's needed for the service to work inside this route
    providers: [ProjectService, importProvidersFrom(HttpClientModule)],
  },
  // ...
];
```

So there you have it, `importProvidersFrom` is like the cardigan you take when you go out in winter just in case.

Following are more features I tried out on a larger local project, and the changes I had to adopt.

Debloating the `main.ts`
------------------------

We can pass all configuration of the `RouterModule` (like `initialNavigation` settings) as we did before, we can also provide all required providers directly into `main.ts` bootstrapper, but now this is how it looks like:

```ts
// a real case of standalone main.ts
bootstrapApplication(AppComponent, {
  providers: [
    // pass the routes from existing RouteModule
    importProvidersFrom(RouterModule.forRoot(AppRoutes, {
      preloadingStrategy: PreloadService,
      paramsInheritanceStrategy: 'always',
      onSameUrlNavigation: 'reload',
      scrollPositionRestoration: 'disabled',
      initialNavigation: 'enabledBlocking'
    }), HttpClientModule),
    Title,
    { provide: LOCALE_ID, useClass: LocaleId },
    { provide: RouteReuseStrategy, useClass: RouteReuseService },
    { provide: TitleStrategy, useClass: CricketTitleStrategy },
    { provide: APP_BASE_HREF, useClass: RootHref }
	   // ... other stuff like APP_INITIALIZER, HTTP_INTERCEPTORS...
  ],
});
```

If we insist on divorcing all `NgModule` it's simpler to export arrays from different files.

We can have multiple `importProvidersFrom` in the same providers array

```ts
// alternative CoreProviders in a separate file (example)
export const CoreProviders = [
  // yes we can have multiple importProvidersFrom
  importProvidersFrom(HttpClientModule),
  Title,
  {
    provide: APP_INITIALIZER,
    useFactory: configFactory,
    multi: true,
    deps: [ConfigService]
  },
  {
    provide: HTTP_INTERCEPTORS,
    useClass: AppInterceptor,
    multi: true,
  },
  { provide: ErrorHandler, useClass: AppErrorHandler }
];

// in app route we can contain AppRouteProviders
export const AppRouteProviders = [
  importProvidersFrom(RouterModule.forRoot(AppRoutes, {
    // ...
  })),
  { provide: RouteReuseStrategy, useClass: RouteReuseService },
  { provide: TitleStrategy, useClass: CricketTitleStrategy },
];

// in main.ts: it becomes less bloated
bootstrapApplication(AppComponent, {
  providers: [
    { provide: LOCALE_ID, useClass: LocaleId },
    { provide: APP_BASE_HREF, useClass: RootHref },
    ...AppRouteProviders,
    ...CoreProviders,
  ],
});
```

The alternative to Module class constructor
-------------------------------------------

In the current module-full setup we had the opportunity to run a script when the module was first injected through **constructors**. Now that we have no constructors, Angular suggests using an existing but undocument token: `ENVIRONMENT_INITIALIZER`. We can use that token within the array of providers, to call scripts in similar fashion. Here is an example.

```ts
// in current module-full, in the AppRoutingModule
// the constructor injects events and observes changes
@NgModule({
  imports: [
    RouterModule.forRoot(AppRoutes, {
      scrollPositionRestoration: 'disabled'
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {
  // how do we replace this logic in the new standalone
  constructor(
    router: Router
  ) {
    router.events.pipe(
      filter(event => event instanceof Scroll)
    ).subscribe({
      next: (e: Scroll) => {
        // some logic
      }
    });
  }
}
```

This can be done inside the `AppRouteProviders` as follows

```ts
// app new standalone routing providers
export const AppRouteProviders = [
  importProvidersFrom(RouterModule.forRoot(AppRoutes, {
    scrollPositionRestoration: 'disabled'
  })),
  {
    // use environment injector
    provide: ENVIRONMENT_INITIALIZER,
    multi: true,
    useValue() {
      // inject first
      const router = inject(Router);
      router.events.pipe(
        filter(event => event instanceof Scroll)
      ).subscribe({
        next: (e: Scroll) => {
          // some logic
        }
      });
    }
  }
];
```

We can also use factories to get the same result

```ts
// app routing providers with factory
const appFactory = (router: Router) => () => {
   router.events.pipe(
    filter(event => event instanceof Scroll)
  ).subscribe({
    next: (e: Scroll) => {
      // some logic
    }
  });
};
export const AppRouteProviders = [
  importProvidersFrom(RouterModule.forRoot(AppRoutes, {
    scrollPositionRestoration: 'disabled'
  })),
  {
    // use environment initializer with factory
    provide: ENVIRONMENT_INITIALIZER,
    multi: true,
    useFactory: appFactory,
    deps: [Router]
  }
];
```

Note that this gets called immediately when added to `main.ts`, while the `APP_INITIALIZER` is programmed to be called when the application is ready, so the order of providers in `main.ts` does not affect `APP_INITIALIZER`, but it affects the environment initializer tokens.

Adding environment tokens to a lazy loaded route provider is also possible, and it is called when *any* route child is loaded.

```ts
// app routing providers with factory
const appFactory = (router: Router) => () => {
   router.events.pipe(
    filter(event => event instanceof Scroll)
  ).subscribe({
    next: (e: Scroll) => {
      // some logic
    }
  });
};
export const AppRouteProviders = [
  importProvidersFrom(RouterModule.forRoot(AppRoutes, {
    scrollPositionRestoration: 'disabled'
  })),
  {
		// use environment initializer with factory
    provide: ENVIRONMENT_INITIALIZER,
    multi: true,
    useFactory: appFactory,
    deps: [Router]
  }
];
```

This however is not related to the single child, but the whole route group, if the route is loaded, all providers' tokens are initialized, in the order they appear in the group. Someone on GitHub proposed a tidier way to [lazily initialize environment](https://github.com/angular/angular/issues/45970) from the parent node, but until then, this is what we have.

### Cannot provide PLATFORM_INITIALIZER

`PLATFORM_INITIALIZER` static provider does not get fired, instead, we can use `ENVIRONMENT_INITIALIZER` in the root bootstrapper.

Building and fixing
-------------------

After building, I realized there is a huge lazy chunk with a very strange name coming out. I was appalled. There was no `common.js` either. Looking deeper, the `main.js` has dropped massively, and the change in size went into the first lazy chunk appearing in `AppRoutes`. This is not bad, but I realized that chunk is immediately loaded with all routes. It probably is wrong naming, it should be `common.js`.

![Standalone bundle sizes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7sydziekvbrroksr5ht0.png)

On server
---------

There is nothing in documentation to insinuate any support for SSR, the closest thing I found on web was this repository [hiepxanh/universatl-test](https://github.com/hiepxanh/universal-test/blob/standalone/server.ts) where the author made adjustments to the `ngExpressEngine` and `CommonEngine`. Given the fact that it is not ready yet, I will not go down that path. Maybe one warm Tuesday evening.

Is it ready?
------------

It is still in preview, and some things are ready, some are not. The issues of todays' preview:

-   Lazy loaded bundle name is not `common.js`
-   CLI design-time does not report any errors, but there are a couple of issues logged in GitHub
-   SSR support is not out of the box, just yet.

If I want to use standalone today, I would for the following purposes

-   Library directives and pipes: I'm in, already
-   Shared components: Yes, yes, yes.
-   Feature common components: usually there is one or a couple shared components, and usually they are light.

I would avoid it for

-   Layout components: if you place your layout components in the application root, this will hardly make any difference given that all the required imports are already available on root. Light common layouts however can make use of the standalone feature.
-   App root, bootstrapping, and core modules: no immediate gain
-   Feature modules: more organized way to contain multiple components that have shared resources.

Thank you for reading all the way to the bottom. Did you smell any rotten tomatoes? Let me know.

### RESOURCES

-   [StackBlitz project](https://stackblitz.com/edit/angular-standalone-app-full)
-   [Angular Standalone documentation](https://angular.io/guide/standalone-components)

{% embed https://garage.sekrab.com/posts/how-to-turn-an-angular-app-into-standalone-part-ii %}