DEV Community

Cover image for Angular Standalone in SSR: update
Ayyash
Ayyash

Posted on • Originally published atgarage.sekrab

Angular Standalone in SSR: update

[TLDR]

To allow all components to be standalone and build for SSR in Angular using the still-supportedngExpressEngine,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
});
Enter fullscreen mode Exit fullscreen mode

Let's rant a bit about it:

Version 16.0

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

// previously app.server.module
@NgModule({
imports:[
NoopAnimationsModule,
ServerModule
],
bootstrap:[AppComponent]
})
exportclassAppServerModule{}
Enter fullscreen mode Exit fullscreen mode

Then inmain.server.tsorserver.ts:

// exported to be used in expressJS
exportconstAppEngine=ngExpressEngine({
bootstrap:AppServerModule
});
Enter fullscreen mode Exit fullscreen mode

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

Bootstrapping

Going to thesource codeof theCommonEnginewe have this:

functionisBootstrapFn(value:unknown):valueis()=>Promise<ApplicationRef>{
// We can differentiate between a module and a bootstrap function by reading `cmp`:
returntypeofvalue==='function'&&!('ɵmod'invalue);
}
Enter fullscreen mode Exit fullscreen mode

That wasn't there before. So the newbootstrappedapplication 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 aPromise<ApplicationRef>

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

We've seen this before. In the browser'sbootstrapApplication.

bootstrapApplication(AppComponent);// returns Promise<ApplicationRef>
Enter fullscreen mode Exit fullscreen mode

Syntax digging

I could not figure out how to assign that function as a value tobootstrapproperty ofngExpressEngineuntil I saw thisGithub issue 3112.Which led me to this commitcommit

Note: The documentation of Angular states nothing, and the downloadable files don't have anything, so you cannot depend on it.

exportdefault()=>bootstrapApplication(AppComponent,{
providers:[
importProvidersFrom(ServerModule),
provideRouter([{path:'shell',component:AppShellComponent}]),
],
});
Enter fullscreen mode Exit fullscreen mode

So our server file should include at least the following:

exportconstAppEngine=ngExpressEngine({
bootstrap:()=>bootstrapApplication(AppComponent)// at least
});
Enter fullscreen mode Exit fullscreen mode

Runningbuildfor SSR in my application, then heading to the host folder and running the server. There are no changes to theExpress 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 theHttpClient)
  • theServerModule

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 tochoose 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
exportconstAppEngine=ngExpressEngine({
bootstrap:_app
});
Enter fullscreen mode Exit fullscreen mode

Building for SSR, testing withmultilingual 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.

Version 17.0

Installing the newrcversion of 17.0 and addingssrsupport viangcli: (RC documentation for SSR), theserver.tsnow uses theCommonEnginedirectly, and the followingengineis gone:

server.engine('html',ngExpressEngine({
bootstrap:AppServerModule,
}));
Enter fullscreen mode Exit fullscreen mode

Here is what comes out of it

// the new server.ts
importbootstrapfrom'./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));
});
Enter fullscreen mode Exit fullscreen mode

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
exportdefault()=>bootstrapApplication(AppComponent);
Enter fullscreen mode Exit fullscreen mode

It may look like using theCommonEnginedirectly is more flexible and gives more options, but knowing that I will have to use that in myNodeJscode, 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 devastatinggenocide of Gaza.

RESOURCES

RELATED POSTS

Top comments(2)

Collapse
armen96work profile image
armen96work

Is there a way to get Hostname with @angular/ssr?

Collapse
ayyash profile image
Ayyash

you can inject theREQUESTin the service and get req.headers('host') like this:
garage.sekrab /posts/loading-ex...

Or you can provide it from NodeJs into its own variable and inject the variable itself
garage.sekrab /posts/loading-ex...

constructor(
// inject our serverURL
@Optional() @Inject('serverUrl') private serverUrl: string
) {}
Enter fullscreen mode Exit fullscreen mode