Enable OneCX app to run in standalone

The goal of this guide is to provide step by step instructions on how to enable standalone mode for an existing OneCX application and allowing it to run without a shell.

General

Prerequisites

This guide assumes that an Angular project is already set up and migrated to be compatible with OneCX.

Note that this guide does not cover general Angular project setup or the steps to migrate to OneCX.

Steps

The goal of this cookbook is to refactor an existing OneCX compatible app so that it can not only run inside the OneCX platform but also as a standalone application. All steps have been tested in an app generated with Angular CLI and migrated to be compatible with OneCX but should also work in other OneCX compatible Angular apps (e.g. apps generated with NX).

To achieve the goal of running an application in standalone, a second set of entrypoint files has to be created. Similarly to the applications remote module in remote scenarios, the standalone entrypoint files will be used to add necessary imports and providers, and route requests to the correct feature modules. A detailed overview of which files have to be created can be found in later sections of this document.

Install @onecx/standalone-shell

In order to run a OneCX app in standalone, it’s necessary to mock/provide some of the functionality that would usually be provided by the OneCX platform. To make this easy, @onecx/standalone-shell provides all the necessary code wrapped in a importable Angular module. Before using the module, the necessary dependency has to be added to the app. The dependency has to be installed using the version that is also used for other @onecx libraries in the project. The minimum required version of @onecx/standalone-shell is 5.31.0.

npm i @onecx/standalone-shell

Rename existing entrypoint files

To avoid confusion, the projects ./src/main.ts and ./src/bootstrap.ts files should be renamed to remote-main.ts and remote-bootstrap.ts. Make sure to also adjust references to ./src/main.ts and ./src/bootstrap.ts (e.g. in tsconfig.app.json and webpack.config.js). If remote-main.ts and remote-bootstrap.ts already exist in the project, no further action is required.

Import StandaloneShellModule and modify app.component.html + app.component.ts

When running an app in standalone, ./src/app/app.module.ts, ./src/app/app.component.html and ./src/app/app.component.ts are treated as the application entrypoints. To add standalone capabilities to the application, the StandaloneShellModule provided by @onecx/standalone-shell has to be added to the imports array of ./src/app/app.module.ts. Make sure to add the import as the last entry of the imports array.

Afterwards the entire application has to be wrapped in a standalone-shell-viewport component provided as part of the imported StandaloneShellModule. To achieve this goal the contents of ./src/app/app.component.html have to be replaced with the following code:

<ocx-standalone-shell-viewport></ocx-standalone-shell-viewport>

Afterwards it should be ensured that the applications remote entrypoint (e.g. ./src/app/app-entrypoint.component.ts) and the applications standalone entrypoint (e.g. ./src/app/app.component.ts) have different selector names. The selector name can be found inside the @Component annotation that is placed before the component class in the components TypeScript file.

Add necessary imports and providers to app.module.ts

Since ./src/app/app.module.ts is not loaded in remote scenarios and the projects remote module is skipped in standalone mode it’s crucial that all critical imports and providers are present in both modules. To achieve this goal it’s recommended to create two exported variables in the remote module file before the actual module class. The variable commonImports should contain an array of imports that need to be present in both modules. Similarly the variable commonProviders should contain an array of providers that need to be present in both modules. After creating those variables they should be added to the imports and providers array in both modules. By doing this, the applications remote module is treated as the single source of truth, which helps with preventing import and provider errors.

// Example providers and imports

import { BrowserModule } from '@angular/platform-browser'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { CommonModule } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'

export const commonProviders = [
  provideHttpClient(withInterceptorsFromDi()),
  ...
]

export const commonImports = [
  BrowserModule,
  BrowserAnimationsModule,
  CommonModule,
  ...
]

export class RemoteModule implements DoBootstrap {
  ...
}

Use AppComponent selector in index.html

To correctly render the standalone entrypoint, it has to be referenced in index.html. Therefore the element tag placed inside the body of ./src/index.html must match the selector name of the applications standalone entrypoint (e.g. ./src/app/app.component.ts), which can be obtained as mentioned above.

Create separate main.ts and bootstrap.ts for standalone

To correctly bootstrap and render the standalone version of an application, it’s necessary to create separate main and bootstrap files alongside the already existing files used for microfrontend bootstrapping (e.g. ./src/remote-main.ts and ./src/remote-bootstrap.ts).

The newly created bootstrap file must contain the following code:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch((err) => console.error(err))

The newly created main file should import the bootstrap files and handle potential errors:

import('./bootstrap').catch((err) => console.error(err))

After creating the files, the newly created main files must be referenced in the files array in tsconfig.app.json so that it’s included in the applications bundle. Additionally it should be verified that the build.options.main property in the applications configuration file (e.g. angular.json or project.json) points to the newly created main file.

Optional: Set up @ngx-translate translation loading

To ensure correct translation loading in an application using @ngx-translate a few more modifications have to be applied to ./src/app/app.module.ts.

  1. The package @ngx-translate/core has to be installed in the project.

  2. The routes passed to RouterModule.forRoot() must be wrapped with addInitializeModuleGuard from @onecx/angular-integration-interface. This ensures correct translation loading for each route. Note that the route configuration might be placed in a separate routing module (e.g. ./src/app/app-routing.module.ts) or in the commonImports you created in your applications remote module.

    imports: [
      ...
      RouterModule.forRoot(addInitializeModuleGuard(routes)),
      ...
    ],
  3. TranslateModule.forRoot() has to be configured in the app modules imports array or the commonImports array that is being shared between app.module.ts and the application’s remote module. createTranslateLoader has to be imported from @onecx/angular-utils and ensures that the correct translation files is loaded on app load. If @onecx/angular-utils is not yet installed in the project, it has to be installed first. Note that the property isolate should not be necessary in a pure standalone setup and is only required when adding the TranslateModule configuration to a commonImports array.

    imports: [
      ...
      TranslateModule.forRoot({
        isolate: false,
        loader: {
          provide: TranslateLoader,
          useFactory: createTranslateLoader,
          deps: [HttpClient]
        }
      }),
      ...
    ],
  4. The path to the applications translations is provided automatically by the previously imported StandaloneShellModule. If any errors occur during translation loading, the network tab of the browsers developer tools can be used to verify that the application’s translation files are placed in the correct location.

Disclaimer: If any of the applications feature modules use ngx-translate features, such as the translate pipe, TranslateModule might have to be added to the feature modules imports array.

Verify that the app is running in standalone

After following the above mentioned steps the app should be accessible on its configured URL (e.g. http://localhost:4200).