Using Embedded Data To Provide Request-Specific Application Configuration In Angular 7.2.0
Earlier this week, I looked at using the encodeForJavaScript() function in ColdFusion as a way to embed a JSON payload in a page response such that it could provide request-specific configuration for a Single-Page Application (SPA). Today, I want to close the loop on that concept by looking at how to actually consume such a JSON payload as part of the bootstrapping process in an Angular 7.2.0 application. For this exploration, I'm going to be using ColdFusion to produce the configuration data; but, such a technology choice could easily be replaced by your back-end language of choice.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
Loading configuration data from an embedded page value is not the only way to initialize an Angular application. In fact, just a couple of months ago, I looked at using the APP_INITIALIZER multi-provider in Angular to load configuration data over AJAX (Asynchronous JavaScript and JSON) as part of the bootstrapping process. The AJAX approach works perfectly well; but, there is something nice about having the configuration data available immediately. It mandates that the initial page request be dynamic (ie, not served from a CDN); but, embedding the configuration data greatly simplifies the bootstrapping process and removes a potentially point of failure (the network request).
That said, in this exploration, I'm going to be using ColdFusion to embed some request-specific configuration data. For the sake of the demo, this data will just be a randomized company selection. On the ColdFusion server, this data will be serialized as JSON and embedded in the HTML page response. Then, on the client, the browser will parse the JSON and make the resulting object available on the global scope:
<cfscript>
// Simulate request-specific, dynamic configuration using random values.
switch ( randRange( 1, 3 ) ) {
case "1":
company = {
"id": 1,
"name": "Acme Corp",
"established": 1908
};
break;
case "2":
company = {
"id": 2,
"name": "Evil Corp",
"established": 2007
};
break;
case "3":
company = {
"id": 3,
"name": "Happy Corp",
"established": 1980
};
break;
}
config = {
"company": company,
"version": "2019.01.11.6.19.0"
};
</cfscript>
<!--- ------------------------------------------------------------------------------ --->
<!--- ------------------------------------------------------------------------------ --->
<!--- Reset the output buffer. --->
<cfcontent type="text/html; charset=utf-8" />
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
Using Embedded Data To Provide Request-Specific Application Configuration In Angular 7.2.0 (CFML Version)
</title>
<cfoutput>
<script type="text/javascript">
// Since the Angular application needs to be configured on a per-request
// basis, we can provide the ColdFusion data in the JavaScript context by
// serializing the data on the ColdFusion server and then parsing it on the
// client as JSON.
// --
// NOTE: By using the encodeForJavaScript() function, we are preventing the
// injection of Cross-Site Scripting (XSS) attacks in any user-provided
// content that may exist within the configuration payload (ex, company.name).
window.appConfig = JSON.parse( "#encodeForJavaScript( serializeJson( config ) )#" );
</script>
</cfoutput>
</head>
<body>
<h1>
Using Embedded Data To Provide Request-Specific Application Configuration In Angular 7.2.0 (CFML Version)
</h1>
<my-app></my-app>
</body>
</html>
So far, this just recaps what we saw in my previous post. In order to prevent Cross-Site Scripting (XSS) attacks, I'm using the Open Web Application Security Project (OWASP) recommendation to quote the value in a JavaScript execution context and then use the encodeForJavaScript() function to interpolate the serialized payload. Then, on the client, we're using the JSON.parse() method to rehydrate the payload and store it at "window.appConfig".
Now, let's look at how we pull that "window.appConfig" value into our Angular application.
Since "window.appConfig" is a globally-available value, we could theoretically reference it from any part of our application that requires configuration data. But, this would tightly couple our services to the current mode of procuring said configuration data. As such, if we wanted to switch from an embedded approach to an AJAX-driven approach at some point in the future, we'd have to go through the application and change all relevant parts of the codebase.
To keep our services blissfully unaware of the mode in which the configuration data is loaded, we want to use Dependency-Injection (DI). In this case, I'm opting to create a single "AppConfig" service that can be injected into any other service in our Angular application. This AppConfig service makes for low-coupling; but, it also provides us with a chance to explicitly define our configuration data-type for enhanced type-safety in TypeScript:
export interface Company {
id: number;
name: string;
established: number;
}
// NOTE: Using name "AppConfig" for "Declaration Merging".
export interface AppConfig {
company: Company;
version: string;
}
// NOTE: Using name "AppConfig" for "Declaration Merging".
export abstract class AppConfig {
// By creating an Abstract Class that has the same name as an Interface, we get to
// leverage "Declaration Merging" in TypeScript. This tells TypeScript to assume
// that this "AppConfig" class will implement the "AppConfig" interface without
// having to actually implement the interface in the class definition. This gives us
// some wiggle-room to use a dynamic, runtime implementation while still getting the
// benefits of type-safety.
// --
// AND, we get to use the Abstract Class as a dependency-injection token since it
// represents a "Type" within our application.
}
In this TypeScript file, notice that we are using both an Interface and an Abstract Class to define the AppConfig API. This two-part definition leverages a TypeScript feature known as "Declaration Merging"; and, it allows us to tell the TypeScript compiler what API the AppConfig instance will have without actually having to implement said API in the AppConfig class definition.
The Abstract Class then acts as both our "Type" and our Dependency-Injection (DI) token for Angular's DI container. In fact, in our App module, we're doing to wire the "window.appConfig" global value into DI container as the AppConfig provider:
// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
// Import the application components and services.
import { AppComponent } from "./app.component";
import { AppConfig } from "./app.config";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// The contextual page request has embedded configuration data into the JavaScript
// execution context using the window.appConfig key. Since we don't want to couple the
// rest of our Angular application to this implementation detail, we're going to map
// this config object onto an injectable Type that also provides us with type-safety.
export function getAppConfig() : AppConfig {
var config: AppConfig = ( <any>window ).appConfig;
// Assert that the page request provided the configuration.
if ( ! config ) {
throw( new Error( "Application bootstrapping could not locate 'window.appConfig' value." ) );
}
return( config );
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@NgModule({
imports: [
BrowserModule
],
declarations: [
AppComponent
],
providers: [
// Provide the AppConfig as an injectable Type for the rest of the application
// components and services.
{
provide: AppConfig,
useFactory: getAppConfig
}
],
bootstrap: [
AppComponent
]
})
export class AppModule {
// ...
}
As you can see, our AppConfig Dependency-Injection token is being defined by the factory function, getAppConfig(). This function is the only part of the Angular codebase that is coupled to "window.appConfig" - it reaches into the global scope and returns embedded configuration payload.
Once the "window.appConfig" is transformed into the injectable AppConfig service, we can then inject it into other part of the Angular application. In this case, we're just going to inject it into the AppComponent:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { AppConfig } from "./app.config";
import { Company } from "./app.config";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
template:
`
<h2>
Dynamic Company Configuration
</h2>
<ul>
<li>
<strong>ID:</strong> {{ company.id }}
</li>
<li>
<strong>Name:</strong> {{ company.name }}
</li>
<li>
<strong>Establised:</strong> {{ company.established }}
</li>
</ul>
`
})
export class AppComponent {
public company: Company;
// I initialize the app component.
constructor( config: AppConfig ) {
console.group( "App Component Constructor" );
console.log( "App Config" );
console.log( config );
console.groupEnd();
this.company = config.company;
}
}
Notice, again, that we are using our AppConfig abstract class as both the Type and the Dependency-Injection token in our AppComponent's constructor. This couples our AppComponent to the concept of the configuration data itself, and not to the means in which the configuration data was obtained.
Now, if we run this ColdFusion and Angular application in the browser, we get the following output:
As you can see, our AppComponent was able to consume the AppConfig service, which was defined from our embedded, page-specific JSON configuration payload.
One of the most appealing features of Angular is its flexibility. In the past, we've looked at using AJAX to load configuration data during the bootstrapping process. And now, we've looked at using embedded, request-specific JSON payloads to pull configuration data into the Dependency-Injection container. Both approaches work well and have merit. But, if you're already running in a dynamic context (ie, with server-side code), there's something very simple and appealing about embedding configuration data right in the page that serves up your Single-Page Application.
Want to use code from this post? Check out the license.
Reader Comments
Out of curiosity why don't we use Angular Universal for the backend application, I believe it will be better solution.
@Murali,
I have not tried Angular Universal before. Can you give me some high-level points on why it would make a better solution? I'm not completely familiar with what it does.