The Angular framework was designed with flexibility in mind. That approach allows Angular applications to be executed across different environments — browser, server, web-worker, and even mobile devices are possible.

In this series of articles, I’m going to reveal to you how does it even possible — execute Angular applications across different environments. Also, we’ll learn how to build custom Angular platform which renders applications inside the system’s terminal using ASCII graphics.

Articles:


That article is the last one in the Angular platforms in-depth series. During the article, I’m going to guide you through the custom platform creation process. But before we start, please, make sure you understand how Angular platforms work or just check previous articles from the series.

As I stated in the first article of the series, Angular is allowed to be executed across different environments because of abstraction. A pretty big part of the Angular is declared as abstract classes and when we’re using different platforms those platforms provide its own implementations for those abstract classes. That’s why to create a terminal platform we just need to provide a number of implementations of services for Angular.

Table of contents

  • Renderer
  • Sanitizer
  • Error handling
  • Terminal module
  • Platform terminal
  • Building terminal application

Renderer

Let’s start with the most important part of the platform terminal — renderer. Angular utilizes Renderer abstraction to perform application rendering in an environment-agnostic way. The renderer is just an abstract class, so we’re going to create an implementation which will render applications inside the system terminal using ASCII graphics.

But wait, how will we render applications inside the system’s terminal? I think the easiest way is to find an appropriate library which is able to create some widgets in terminal using ASCII graphics. I decided to use blessed — a curses-like library with a high-level terminal interface API for node.js. First of all, let’s install the library with appropriate typings:

npm install blessed @types/blessed

As I decided to use a specific library for UI rendering the only thing I need to do here is to build some kind of adapter. We need to find a way to match Angular renderer’s API with the API of the blessed library.

Well, here is a simplified declaration of the Angular Renderer:

export abstract class Renderer2 {

  abstract createElement(name: string, namespace?: string|null): any;

  abstract createText(value: string): any;

  abstract appendChild(parent: any, newChild: any): void;

  abstract addClass(el: any, name: string): void;

  abstract removeClass(el: any, name: string): void;

  // ...
}

Its responsibility is to create and remove elements, add classes and attributes, register event listeners and so forth.

On the other hand, we have the blessed library with the following interface:

const blessed = require('blessed');

// Create blessed screen
const screen = blessed.screen();

// Create some elements
const box = blessed.box();
const table = blessed.table();

// Add elements on the screen
table.append(box);
screen.append(table);

// Display all changes
screen.render();

As you could notice here, blessed is a plain node.js library, which provides a screen and a number of components here. Screen is a kind of browser’s document. It serves as a root element of the application, also as contains a number of useful API’s.

Screen

Let’s start renderer-blessed integration with screen. First of all, let’s create a separate Screen service which will provide a screen for the renderer.

import { Injectable } from '@angular/core';

import * as blessed from 'blessed';
import { Widgets } from 'blessed';

@Injectable()
export class Screen {

  private screen: Widgets.Screen;

  constructor() {
    this.screen = blessed.screen({ smartCSR: true });
    this.setupExitListener();
  }

  selectRootElement(): Widgets.Screen {
    return this.screen;
  }

  private setupExitListener() {
    this.screen.key(['C-c'], () => process.exit(0));
  }
}

Here we have a basic Screen implementation. The service is responsible for creating a blessed screen as it was stated above, also as set up an exit listener. Because we’re going to execute our applications inside the terminal, and it’s a pretty common behavior when terminal applications are closing when pressing control+c. That’s why we need to listen for that event on the screen and exit process as a reaction. process.exit is a standard node.js API, which allows a script to exit itself with the appropriate code. 0 means that process just completed without any errors. Also, here we could see that Screen provides the ability to select a root element for our application through selectRootElement call.

Elements registry

When we have Screen and can select the root element it’s a time to deal with the elements creation process. As I stated above, blessed provides the ability to create elements on the screen through the set of functions exported directly through the blessed package. Meanwhile, Angular renderer utilizes a single createElement function. That’s why we need to create some kind of adapter for that element creation logic. For instance, I decided to wrap blessed elements creation logic in special service — ElementsRegistry. ElementsRegistry is responsible for the creation blessed elements through single createElement function:

import { Injectable } from '@angular/core';
import * as blessed from 'blessed';
import { Widgets } from 'blessed';

export type ElementFactory = (any) => Widgets.BoxElement;

export const elementsFactory: Map<string, ElementFactory> = new Map()
  .set('text', blessed.text)
  .set('box', blessed.box)
  .set('table', blessed.table)

@Injectable()
export class ElementsRegistry {

  createElement(name: string, options: any = {}): Widgets.BoxElement {
    let elementFactory: ElementFactory = elementsFactory.get(name);

    if (!elementFactory) {
      elementFactory = elementsFactory.get('box');
    }

    return elementFactory({ ...options, screen: this.screen });
  }
}

So, as you can notice here, ElementsRegistry has a single createElement method, which actually tries to find the requested element in elements map and return an instance of that element. In case of the element wasn’t find ElementsRegistry will fallback to the box element, which is analog for div element in the browser.

On that stage, we have all the required entities to create Angular Renderer which will render applications directly inside the system’s terminal using ASCII graphics.

Renderer

Here is the basic renderer implementation.

export class TerminalRenderer implements Renderer2 {

  constructor(private screen: Screen, private elementsRegistry: ElementsRegistry) {
  }

  createElement(name: string, namespace?: string | null): any {
    return this.elementsRegistry.createElement(name);
  }

  createText(value: string): any {
    return this.elementsRegistry.createElement('text', { content: value });
  }

  selectRootElement(): Widgets.Screen {
    return this.screen.selectRootElement();
  }

  appendChild(parent: Widgets.BlessedElement, newChild: Widgets.BlessedElement): void {
    parent.append(newChild);
  }

  setAttribute(el: Widgets.BlessedElement, name: string, value: string, namespace?: string | null): void {
    el[name] = value;
  }

  setValue(node: Widgets.BlessedElement, value: string): void {
    node.setContent(value);
  }
}

I’ve decided to implement only the small part of all required methods here, leaving the rest of the implementations for you. So, here we have TerminalRenderer class which implements Renderer2 interface. It utilizes Screen and ElementsRegistry created above to perform elements creation.

But look, TerminalRenderer is not an Injectable service here. Angular requires Renderer to be created through the RendererFactory. So, let’s add one:

@Injectable()
export class TerminalRendererFactory implements RendererFactory2 {
  
  constructor(private screen: Screen, private elementsRegistry: ElementsRegistry)

  createRenderer(): Renderer2 {
    return new TerminalRenderer(this.screen, this.elementsRegistry);
  }
}

TerminalRendererFactory here implements RendererFactory2 interface and implements only one method — createRenderer which actually creates a new instance of TerminalRenderer with required services.

On that stage, we have full featured Angular TerminalRenderer which is able to render Angular applications inside the system’s terminal using ASCII graphics. But we still need to add a lot, so, let’s dig deeper 🔥

Sanitizer

Angular utilizes Sanitizer abstraction to sanitize potentially dangerous values. It’s required by the Angular to bootstrap the application. As Sanitizer is declared as an abstract class in the Angular core package, Browser platform provides its own implementation — DomSanitizer. DomSanitizer helps to prevent Cross Site Scripting attacks by sanitizing values to be safely used in the DOM.

For instance, when binding a URL in an <a [href]=”someValue”> hyperlink, someValue will be sanitized so that an attacker cannot inject e.g. a javascript: URL that would execute code on the website. In specific situations, it might be necessary to disable sanitization, for example, if the
application genuinely needs to produce a javascript: style link with a dynamic value in it. Users can bypass security by constructing a value with one of the bypassSecurityTrust... methods, and then binding to that value from the template.

But inside the terminal we haven’t got DOM, that’s why Cross-Site Scripting attacks can’t be performed. But, if Cross-Site Scripting attacks can’t be performed, then, we don’t need to sanitize any templates values. That’s why we can provide just an empty implementation of Sanitizer to the Angular:


import { Sanitizer, SecurityContext } from '@angular/core';

export class TerminalSanitizer extends Sanitizer {
  sanitize(context: SecurityContext, value: string): string {
    return value;
  }
}

As you can notice here, that TerminalSanitizer implementation just does nothing, it just returns accepted value.

Error handling

Every good application has to know how to deal with errors properly. Angular applications are not exclusions. That’s why Angular provides the ability to set up global ErrorHandler which will react on each unhandled exception in your applications. Angular provides a default implementation for the ErrorHandler which utilizes a browser console to log all unhandled exceptions properly. But in it’s not enough for the terminal.

In the terminal, we have an additional issue — if an application throws an exception somewhere, ErrorHandler will just log it. But the application will remain stuck. In the browser, we could just reload the tab with the application but it’s impossible to do right in the terminal. That’s why we need to provide a custom implementation for the ErrorHandlerwhich will not only log the exception but also exit the current process:

import { ErrorHandler, Injectable } from '@angular/core';

@Injectable()
export class TerminalErrorHandler implements ErrorHandler {

  handleError(error: Error): void {
    console.error(error.message, error.stack);
    process.exit(1);
  }
}

Here is the basic implementation for the ErrorHandler. It just logs the error in the console and then exits the process with the error code 1 which means that something went wrong during the application execution.

Terminal module

As you remember, each Angular application created with the Angular CLI configured to be executed in the browser by default, that’s why it contains BrowserModule imported in the AppModule. BrowserModule contains a number of browser-specific providers, also as reexports CommonModuleand ApplicationModule. Those modules contain multiple crucial providers for Angular applications. The terminal platform also requires those providers, that’s why we need to create a custom TerminalModule which will reexport CommonModule and ApplicationModule, also as register part of created above services in the application.

import { CommonModule, ApplicationModule, ErrorHandler, NgModule, RendererFactory2 } from '@angular/core';

import { Screen } from './screen';
import { ElementsRegistry } from './elements-registry';
import { TerminalRendererFactory } from './renderer';
import { TerminalErrorHandler } from './error-handler';

@NgModule({
  exports: [CommonModule, ApplicationModule],
  providers: [
    Screen,
    ElementsRegistry,
    { provide: RendererFactory2, useClass: TerminalRendererFactory },
    { provide: ErrorHandler, useClass: TerminalErrorHandler },
  ],
})
export class TerminalModule {
}

But not all services could be registered through TerminalModule some of them are required during the bootstrap phase and have to be provided before. The only way to provide services, in that case, is to create a custom platform

Platform terminal

import { COMPILER_OPTIONS, createPlatformFactory, Sanitizer } from '@angular/core';
import { ɵplatformCoreDynamic as platformCoreDynamic } from '@angular/platform-browser-dynamic';
import { DOCUMENT } from '@angular/common';
import { ElementSchemaRegistry } from '@angular/compiler';

import { TerminalSanitizer } from './sanitizer';


const COMMON_PROVIDERS = [
  { provide: DOCUMENT, useValue: {} },
  { provide: Sanitizer, useClass: TerminalSanitizer, deps: [] },
];

export const platformTerminalDynamic = createPlatformFactory(platformCoreDynamic,
  'terminalDynamic', COMMON_RPOVIDERS]);

Platform terminal here is created through createPlatformFactory function which allows us to inherit platformCoreDynamic providers. Also as add platform terminal specific providers.

On that stage, we’ve done all the things and it’s a time to build an Angular application for the terminal.

Building terminal application

First of all, let’s create a new Angular application using Angular CLI:

ng new AngularTerminalApp

Then, let’s add TerminalModule in the AppModule imports section:

import { NgModule } from '@angular/core';
import { TerminalModule } from 'platform-terminal';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    TerminalModule,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {
}

When AppModule is done it’s a time to set up terminal platform:

import { platformTerminalDynamic } from 'platform-terminal';
import { enableProdMode } from '@angular/core';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformTerminalDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

Here you could notice how terminal platform imported from the platform-terminal package we built previously. And then used to bootstrap our application’s AppModule.

The only thing we need to do here is to createAppComponent with all the required elements for the application:

import { ChangeDetectionStrategy, Component } from '@angular/core';

import { TransactionsService } from '../transactions.service';
import { SparklineService } from '../sparkline.service';
import { ServerUtilizationService } from '../server-utilization.service';
import { ProcessManagerService } from '../process-manager.service';

@Component({
  selector: 'app-component',
  template: `
    <grid rows="12" cols="12">
      <line
        [row]="0"
        [col]="0"
        [rowSpan]="3"
        [colSpan]="3"
        label="Total Transactions"
        [data]="transactions$ | async">
      </line>
      <bar
        [row]="0"
        [col]="3"
        [rowSpan]="3"
        [colSpan]="3"
        label="Server Utilization (%)"
        [barWidth]="4"
        [barSpacing]="6"
        [xOffset]="3"
        [maxHeight]="9"
        [data]="serversUtilization$ | async">
      </bar>
      <line
        [row]="0"
        [col]="6"
        [rowSpan]="6"
        [colSpan]="6"
        label="Total Transactions"
        [data]="transactions$ | async">
      </line>
      <table
        [row]="3"
        [col]="0"
        [rowSpan]="3"
        [colSpan]="6"
        fg="green"
        label="Active Processes"
        [keys]="true"
        [columnSpacing]="1"
        [columnWidth]="[28,20,20]"
        [data]="process$ |async">
      </table>
      <map
        [row]="6"
        [col]="0"
        [rowSpan]="6"
        [colSpan]="9"
        label="Servers Location">
      </map>
      <sparkline
        row="6"
        col="9"
        rowSpan="6"
        colSpan="3"
        label="Throughput (bits/sec)"
        [tags]="true"
        [style]="{ fg: 'blue', titleFg: 'white', border: {} }"
        [data]="sparkline$ | async">
      </sparkline>
    </grid>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  transactions$ = this.transactionsService.transactions$;
  sparkline$ = this.sparklineService.sparkline$;
  serversUtilization$ = this.serversUtilization.serversUtilization$;
  process$ = this.processManager.process$;

  constructor(private transactionsService: TransactionsService,
              private sparklineService: SparklineService,
              private serversUtilization: ServerUtilizationService,
              private processManager: ProcessManagerService) {
  }
}

AppComponent is pretty dumb here, so, I’ll leave it without comments.

Then we need to compile the application somehow. It could be done using Angular Compiler CLI:

ngc -p tsconfig.json

Angular Compiler CLI will produce compiled application files in the dist folder right in the root of the project folder. So, we just need to bootstrap it as a plain node.js application:

node ./dist/main.js

And then we’ll see:

Conclusion

Congratulations 🥳 you’ve reached the end of the article. During the article, we’ve learned a lot about Angular platforms. Went through a custom platform creation process. Learned about crucial Angular services and modules and finally, built custom platform which renders Angular applications inside the system’s terminal using ASCII graphics!

Here is the repository with all the source files related to the Terminal platform: https://github.com/Tibing/platform-terminal

If you want to get deeper knowledge on Angular platforms, take a look at the rest articles of the series:

Also, follow me on twitter to be notified about new Angular articles as soon as possible!