Loading external configurations inline and in SSR in Angular
Tags: angular, javascript, tutorial
Published: 2022-03-25 12:35:11

In the previous article I made use of the APP_INITLIZER token to [load external configurations via HTTP](https://elmota.com/loading-external-configurations-via-http-using-appinitializer). Today I am going to bring the configuration closer, ditching the HTTP request. But **how do we inject json into HTML**, in an Angular application?

The implementation needs to meet two targets:

- The configuration cannot be included in the compiled source, thus it **cannot be imported** directly, or indirectly in typescript.  
This rules out the local import:  
`import * as WebConfig from '/localdata/config.json';`  
Or the module script  
`<script type="module" src="/localdata/config.js">`  
Or dynamic module loading  
```typescript
import('./localdata/config.js')  
  .then((config) => {
    // do something with config
  });
```
- We want to maintain typing, so `config` cannot be used before it is casted. 

Since JSON cannot be injected in HTML due to security precautions, let me create the configuration script:
```javascript
// configs/config.js file, named it "WebConfig" to avoid confusion
const WebConfig = {
  isServed: true,
  API: {
    apiRoot: 'url/server/app',
  },
  MyKey: 'MyValue',
};
```
### Injecting a script

The only location to import a JavaScript config without including it in the build, is **directly in HTML header**. It's the only place that does not get checked at design time, and throws a silent 404 at runtime.

This is how it's done.
```
<script src="localdata/config.js"></script>
```
To make this path work, an adjustment in `angular.json assets` is needed:  
> I make it a habit to name things differently just to remember that the rule exists.

``` JSON
{ //... angular.json
"assets": [
  {
    "glob": "*",
    "input": "configs",
    "output": "/localdata"
  }
]
```
### Implementing APP_INITIALIZER
Let's build an `APP_INITIALIZER` with minimum response: `void`. Here is the `ConfigService`
```typescript

// declare WebConfig
declare const WebConfig: any;

export const configFactory = (config: ConfigService): (() => void) => {
    return () => config.loadAppConfig();
};

@Injectable({
  providedIn: 'root',
})
export class ConfigService {
  constructor() {}

 // set a static member for easier handling
 private static _config: IConfig;

 static get Config(): IConfig {
    return this._config || Config;
  }

  private _createConfig(config: any): IConfig {
    // cast all keys as are, extend local Config
    const _config = { ...Config, ...(<IConfig>config) };
    // set static member
    ConfigService._config = _config;
    return _config;
  }

  loadAppConfig(): void {
    // here is the JavaScript variable... is it ready?
    if (WebConfig?.isServed) {
      this._createConfig(WebConfig);
    } else {
      // not loaded? fall back
      console.log('error');
      this._createConfig(Config);
    }
  }
}
```
#### Issues:
First issue to fix is the type of `WebConfig`, declare a const in the same service file:  
`declare const WebConfig: any;`

The other issue is **the extreme case of slow configuration**. If the script has a `defer` property it should not be blocking, and if it is from `localdata` served from same server,** it should be fast enough**. On StackBlitz however, it is too slow. I am not going down that track though, because if we had to take care of "waiting for remote config to load locally", then we are better off with the HTTP method. 

**To tighten the loose ends though, the extreme case is produced locally with the following:**
- Load the config from a remote server
- add `async` attribute
- and probably, place the script before end of body

`<script src="https://saphire.sekrab.com/localdata/config.js" async></script>`

Running... The `WebConfig` has no value initially, so it throws an "undefined" error. To fix that, a patch in `index.html` or in any javascript added to code.
```html
<script>
  window.WebConfig = {
    isServed: false,
  };
</script>
```
### Implementing APP_BOOTSTRAP_LISTENER
The main problem with this listener is that it gets fired after any router resolve, it's too late for configurations so we are not going in that direction.

### Implementing PLATFORM_INITIALIZER
Since the return of the token is not important, we might be able to load it earlier, in Platform Initializer. Though you must be careful, use `defer` and stay local. (PS. cannot use this method on StackBlitz.) 
```typescript
export const platformFactory = (): (() => void)  => {
    ConfigService.loadAppConfig(); // static element
    return () => null;
};
```
In `main.ts`
```typescript
 platformBrowserDynamic([
    {
          provide: PLATFORM_INITIALIZER,
          useFactory: platformFactory,
          multi: true,
     }
 ]).bootstrapModule(AppBrowserModule)
```
This token does not use dependencies, so the `ConfigService` ends up being a **group of static elements**, so no need to provide it anywhere. Let me rewrite and test.
```typescript
// notice it no longer needs to be injected
export class ConfigService {
    private static _config: IConfig;

    static get Config(): IConfig {
        return this._config || Config;
    }

   private static _createConfig(config: any): IConfig {
    // cast all keys as are
    const _config = { ...Config, ...(<IConfig>config) };
    // set static member
    ConfigService._config = _config;
    return _config;
   }
  static loadAppConfig(): void {
     if (WebConfig?.isServed) {
        this._createConfig(WebConfig);
      } else {
       // error
        this._createConfig(Config);
      }
   }
}
```
Let us also just make it local:

`<script src="localdata/config.js" defer></script>`

Using it is as simple as referencing the static element anywhere.  
`ConfigService.Config.isServed`

The router resolve also withstood the test, since `defer` attribute loads the JavaScript after parsing, but before `DOMContentLoaded`. On client side, it all works. Now on to **SSR**.

### SSR

If we use `APP_INITIALIZER` (with static methods), the token is still provided in `AppModule`, which is shared for both platforms. If we use `PLATFORM_INITIALIZER`, it has been injected in `platformBrowserDynamic` which only runs browser platform. For SSR, needs to be injected in server platform. 

In `server.ts`, bootstrapping `AppServerModule` occurs as an option for [ngExpressEngine](https://github.com/angular/universal/tree/master/modules/express-engine), which takes another option: `providers array`, and that is where the token is provided:
```typescript
// in server.ts, or where you create the ngExpressEngine
export const AppEngine = ngExpressEngine({
    bootstrap: AppServerModule,
    // pass provider here
    providers:[
        {
            provide: PLATFORM_INITIALIZER,
            useFactory: platformFactory,
            multi: true,
        }
    ]
});
```
That is not enough. Now the `WebConfig` on the server side is **undefined**. 

In server output folder *after build*, [where the express app is defined](https://stackblitz.com/edit/local-config?file=host%2Fserver.js), the `WebConfig` variable must be set in global context. In `NodeJs` (are we not all using it?) it is as simple as `global.WebConfig`

`global.WebConfig = require('./localdata/config.js');`

The `localdata` in this case is a server folder, that contains the server config.js file.

**But wait** the config.js file must have an `exports` statement for that line to work. Also, it cannot have the `exports` statement to run in browser after hydration! 

**Solution?** check for a property that is null on one platform, and not null on the other. The easiest property is `window`. (you can create one, but it takes 5 times more lines of code to cater for it).

First, in your express server file, set `global.window = undefined`.

Then, in the host config file (server/localdata/config.js):

```js
// in config.js add the following lines 
if (!window) {
    module.exports = WebConfig;
}
```
There. Now the config file shall work on both browser and server platforms.

### Challenges
- **It must be in HTML**, thus, if you choose to differentiate `config.js` from `config.prod.js` you will end up  with `index.dev.html` and `index.html` for production.  
- **It cannot be a json file**, but a JS with a const.
- **It must be local**, remote is too slow and does not work on sever side.
- **To serve SSR, you need the extra baggage **

> [StackBlitz project](https://stackblitz.com/edit/local-config) does not run Angular universal, nevertheless, the source code includes SSR

### A step back, to HTTP
I like the HTTP method! There is one trick though that we can utilize in SSR, to provide the JSON in the HTML Engine render options, for SSR only. What does that entail? What do we need to fix? Let me tell you about it **next week**. Along with how to **separate your server code completely from Angular for better control**.

#### Resources
- [Angular Express Engine](https://github.com/angular/universal/tree/master/modules/express-engine)
- [PLATFORM_INITIALIZER](https://angular.io/api/core/PLATFORM_INITIALIZER)
- [StackBlitz App](https://stackblitz.com/edit/local-config)
- [Defer attribute on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer)