Sekrab Garage

Angular Standalone Feature

Angular Standalone in SSR: update

AngularTip November 6, 23
Subscribe to Sekrab Parts newsletter.
Series:

Standalone

In this article, we will turn some components in an existing Angular app into standalone components, to see if this feature is worth the trouble. What else do they need to work properly? What scenarios are they best used for? How do they affect development experience? and Output bundles?
  1. one year ago
    How to turn an Angular app into standalone - Part I
  2. one year ago
    How to turn an Angular app into standalone - Part II
  3. one year ago
    Angular standalone router providers: an update
  4. one year ago
    Angular 15 standalone HTTPClient provider: Another update
  5. two months ago
    Angular Standalone in SSR: update

[TLDR]

To allow all components to be standalone and build for SSR in Angular using the still-supported ngExpressEngine, here is the solution:

// in server.ts or main.server.ts (exported)
const _app = () => bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(ServerModule),
    // add providers, interceptors, and all routes you want enabled on server
    ...CoreProviders,
    // pass the routes from existing Routes used for browser
    ...AppRouteProviders
  ],
});

// in server.ts, nothing else changes
server.engine('html', ngExpressEngine({
  bootstrap: _app
});

Let’s rant a bit about it:

anchorVersion 16.0

Another update worthy of notice is how the SSR is done in standalone environment, originally the app server module looked like this

// previously app.server.module
@NgModule({
  imports: [
    NoopAnimationsModule,
    ServerModule
  ],
  bootstrap: [AppComponent]
})
export class AppServerModule { }

Then in main.server.ts or server.ts:

// exported to be used in expressJS
export const AppEngine = ngExpressEngine({
  bootstrap: AppServerModule
});

This is how we did it when we created our isolated Express server. I want to continue with that line of work, and investigate the newly, undocumented feature, to allow SSR (Angular Universal) to run standalone components.

anchorBootstrapping

Going to the source code of the CommonEngine we have this:

function isBootstrapFn(value: unknown): value is () => Promise<ApplicationRef> {
  // We can differentiate between a module and a bootstrap function by reading `cmp`:
  return typeof value === 'function' && !('ɵmod' in value);
}

That wasn’t there before. So the new bootstrapped application is supported in v16.0. No need to wait to v17.0. That’s good news. The bootstrap property now expects either a module, or a function that returns a Promise<ApplicationRef>

bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);

We’ve seen this before. In the browser’s bootstrapApplication.

bootstrapApplication(AppComponent); // returns Promise<ApplicationRef>

anchorSyntax digging

I could not figure out how to assign that function as a value to bootstrap property of ngExpressEngine until I saw this Github issue 3112. Which led me to this commit commit

Note: The documentation of Angular states nothing, and the downloadable files don’t have anything, so you cannot depend on it.
export default () => bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(ServerModule),
    provideRouter([{ path: 'shell', component: AppShellComponent }]),
  ],
});

So our server file should include at least the following:

export const AppEngine = ngExpressEngine({
	bootstrap: () => bootstrapApplication(AppComponent) // at least
});

Running build for SSR in my application, then heading to the host folder and running the server. There are no changes to the Express server. It builds, and it loads, an empty screen.

What we need to add is:

  • the routes we want rendered on the server
  • browser providers needed in server environment (like the HttpClient)
  • the ServerModule

In the example given only the shell component is provided, I usually want the whole app to be server-rendered but it is more flexible now to choose which routes to render. Also, there are a lot of things provided in browser module, that need to be provided again. So what I did is simply provide the whole thing from the browser application:

const _app = () => bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(ServerModule),
    // add providers, interceptors, and all routes you want enabled on server (Examples)
    { provide: LOCALE_ID, useClass: LocaleId },
    { provide: APP_BASE_HREF, useClass: RootHref },
    // provide same providers for the browser, like HttpInterceptors, APP_INITIALIZER...
    ...CoreProviders,
    // pass the routes from existing Routes
    ...AppRouteProviders
  ],
});

// export the bare minimum, let nodejs take care of everything else
export const AppEngine = ngExpressEngine({
  bootstrap: _app
});

Building for SSR, testing with multilingual URL driven with prepared index files, and I can confirm it works. I still want to dig deeper though, but I’ll leave it for another Tuesday.

anchorVersion 17.0

Installing the new rc version of 17.0 and adding ssr support via ng cli: (RC documentation for SSR), the server.ts now uses the CommonEngine directly, and the following engine is gone:

server.engine('html', ngExpressEngine({
	bootstrap: AppServerModule,
}));

Here is what comes out of it

// the new server.ts
import bootstrap from './src/main.server';

server.get('*', (req, res, next) => {
  const { protocol, originalUrl, baseUrl, headers } = req;

  commonEngine
    .render({
      bootstrap, // this is exported from main.server.ts
      documentFilePath: indexHtml,
      url: `${protocol}://${headers.host}${originalUrl}`,
      publicPath: distFolder,
      providers: [
        { provide: APP_BASE_HREF, useValue: baseUrl },],
    })
    .then((html) => res.send(html))
    .catch((err) => next(err));
});

Where the bootstrap property is exported like this (it isn't included in the created files that's why there was an issue logged in GitHub about it, someone's bug, is another one's blessing I guess).

// main.server.ts minimum line
export default () => bootstrapApplication(AppComponent);

It may look like using the CommonEngine directly is more flexible and gives more options, but knowing that I will have to use that in my NodeJs code, I am not a big fan. Also, that will affect the prerender builder. I will not dig any deeper because I am not keen on working on unstable versions. Let’s wait for it first.

Thank you for reading all the way. This post has been the hardest to write, since all my brain capacity is drained out following up on the devastating genocide of Gaza.

  1. one year ago
    How to turn an Angular app into standalone - Part I
  2. one year ago
    How to turn an Angular app into standalone - Part II
  3. one year ago
    Angular standalone router providers: an update
  4. one year ago
    Angular 15 standalone HTTPClient provider: Another update
  5. two months ago
    Angular Standalone in SSR: update