20. Cookbook

20.1. Abstract Class Store

The following solution presents a base class for implementing stores which handle state and its transitions. Working with the base class achieves:

  • common API across all stores

  • logging (when activated in the constructor)

  • state transitions are asynchronous by design - sequential order problems are avoided

Listing 84. Usage Example
@Injectable()
export class ModalStore extends Store<ModalState> {

  constructor() {
    super({ isOpen: false }, !environment.production);
  }

  closeDialog() {
    this.dispatchAction('Close Dialog', (currentState) => ({...currentState, isOpen: false}));
  }

  openDialog() {
    this.dispatchAction('Open Dialog', (currentState) => ({...currentState, isOpen: true}));
  }

}
Listing 85. Abstract Base Class Store
import { OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { intersection, difference } from 'lodash';
import { map, distinctUntilChanged, observeOn } from 'rxjs/operators';
import { Subject } from 'rxjs/Subject';
import { queue } from 'rxjs/scheduler/queue';
import { Subscription } from 'rxjs/Subscription';

interface Action<T> {
  name: string;
  actionFn: (state: T) => T;
}

/** Base class for implementing stores. */
export abstract class Store<T> implements OnDestroy {

  private actionSubscription: Subscription;
  private actionSource: Subject<Action<T>>;
  private stateSource: BehaviorSubject<T>;
  state$: Observable<T>;

  /**
   * Initializes a store with initial state and logging.
   * @param initialState Initial state
   * @param logChanges When true state transitions are logged to the console.
   */
  constructor(initialState: T, public logChanges = false) {
    this.stateSource = new BehaviorSubject<T>(initialState);
    this.state$ = this.stateSource.asObservable();
    this.actionSource = new Subject<Action<T>>();

    this.actionSubscription = this.actionSource.pipe(observeOn(queue)).subscribe(action => {
      const currentState = this.stateSource.getValue();
      const nextState = action.actionFn(currentState);

      if (this.logChanges) {
        this.log(action.name, currentState, nextState);
      }

      this.stateSource.next(nextState);
    });
  }

  /**
   * Selects a property from the stores state.
   * Will do distinctUntilChanged() and map() with the given selector.
   * @param selector Selector function which selects the needed property from the state.
   * @returns Observable of return type from selector function.
   */
  select<TX>(selector: (state: T) => TX): Observable<TX> {
    return this.state$.pipe(
      map(selector),
      distinctUntilChanged()
    );
  }

  protected dispatchAction(name: string, action: (state: T) => T) {
    this.actionSource.next({ name, actionFn: action });
  }

  private log(actionName: string, before: T, after: T) {
    const result: { [key: string]: { from: any, to: any} } = {};
    const sameProbs = intersection(Object.keys(after), Object.keys(before));
    const newProbs = difference(Object.keys(after), Object.keys(before));
    for (const prop of newProbs) {
      result[prop] = { from: undefined, to: (<any>after)[prop] };
    }

    for (const prop of sameProbs) {
      if ((<any>before)[prop] !== (<any>after)[prop]) {
        result[prop] = { from: (<any>before)[prop], to: (<any>after)[prop] };
      }
    }

    console.log(this.constructor.name, actionName, result);
  }

  ngOnDestroy() {
    this.actionSubscription.unsubscribe();
  }

}

20.2. Angular Electron

20.2.1. Add Electron to an Angular application

This cookbook recipe explains how to integrate Electron in an Angular 6+ application. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. As an example, very well known applications as Visual Studio Code, Atom, Slack or Skype (and many more) are using Electron too.

Note
At the moment of this writing Angular 7.2.3 and Electron 4.0.2 were the versions available.

Here are the steps to achieve this goal. Follow them in order.

Add Electron and other relevant dependencies

There are two different approaches to add the dependencies in the package.json file:

  • Writing the dependencies directly in that file.

  • Installing using npm install or yarn add.

Important
Please remember if the project has a package-lock.json or yarn.lock file use npm or yarn respectively.

In order to add the dependencies directly in the package.json file, include the following lines in the devDependencies section:

"devDependencies": {
...
    "electron": "^4.0.2",
    "electron-builder": "^20.38.5",
    "electron-reload": "^1.4.0",
    "npm-run-all": "^4.1.5",
    "npx": "^10.2.0",
    "wait-on": "^3.2.0",
    "webdriver-manager": "^12.1.1"
...
},

As indicated above, instead of this npm install can be used:

$ npm install -D electron electron-builder electron-reload npm-run-all npx wait-on webdriver-manager

Or with yarn:

$ yarn add -D electron electron-builder electron-reload npm-run-all npx wait-on webdriver-manager
Add Electron build configuration

In order to configure electron builds properly a electron-builder.json must be included in the root folder of the application. For more information and fine tuning please refer to the Electron Builder official documentation.

The contents of the file will be something similar to the following:

{
  "productName": "app-name",
  "directories": {
    "output": "release/"
  },
  "files": [
    "**/*",
    "!**/*.ts",
    "!*.code-workspace",
    "!LICENSE.md",
    "!package.json",
    "!package-lock.json",
    "!src/",
    "!e2e/",
    "!hooks/",
    "!angular.json",
    "!_config.yml",
    "!karma.conf.js",
    "!tsconfig.json",
    "!tslint.json"
  ],
  "win": {
    "icon": "dist/assets/icons",
    "target": ["portable"]
  },
  "mac": {
    "icon": "dist/assets/icons",
    "target": ["dmg"]
  },
  "linux": {
    "icon": "dist/assets/icons",
    "target": ["AppImage"]
  }
}

Theres two important things in this file:

  1. "output": this is where electron builder is going to build our application

  2. "icon": in every OS possible theres an icon parameter, the route to the icon folder that will be created after building with angular needs to be used here. This will make it so the electron builder can find the icons and build.

Create the necessary typescript configurations

In order to initiate electron in an angular app we need to modify the tsconfig.json file and create a new one named tsconfig-serve.json in the root folder.

tsconfig.json

This file needs to be modified to add the main.ts and src/**/* folders excluding the node_modules:

{
....
  },
  "include": [
    "main.ts",
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
....
}
tsconfig-serve.json

In the root, tsconfig-serve.json needs to be created. This typescript config file is going to be used when we serve electron:

{
  "compilerOptions": {
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2017",
      "es2016",
      "es2015",
      "dom"
    ]
  },
  "include": [
    "main.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}
Modify angular.json

angular.json has to to be modified so the project is build inside /dist without an intermediate folder.

{
....
  "architect": {
    ....
    "build": {
      outputPath": "dist",
      ....
}
Add Angular Electron directives

In order to use Electron’s webview tag and its methods inside an Angular application our project needs the directive webview.directive.ts file. We recommend to create this file inside a shared module folder, although it has to be declared inside the main module app.module.ts.

Listing 86. File webview.directive.ts
import { Directive } from '@angular/core';

@Directive({
  selector: '[webview]',
})
export class WebviewDirective {}
Add access Electron APIs

To call Electron APIs from the Renderer process, install ngx-electron module.

With npm:

$ npm install ngx-electron --save

Or with yarn:

$ yarn add ngx-electron --save

This package contains a module named NgxElectronModule which exposes Electron APIs through a service called ElectronService

Update app.module.ts and app-routing.module.ts

As an example, the webview.directive.ts file is located inside a shared module:

Listing 87. File app.module.ts
// imports
import { NgxElectronModule } from 'ngx-electron';
import { WebviewDirective } from './shared/directives/webview.directive';

@NgModule({
  declarations: [AppComponent, WebviewDirective],
  imports: [
    ...
    NgxElectronModule
    ...
    ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Here NgxElectronModule is also added so ElectronService can be injected wherever is needed.

After that is done, the use of hash has to be allowed so electron can reload content properly. On the app-routing.module.ts:

....
  imports: [RouterModule.forRoot(routes,
    {
      ....
      useHash: true,
    },
  )],
Usage

In order to use Electron in any component class the ElectronService must be injected:

import { ElectronService } from 'ngx-electron';

...

constructor(
  // other injected services
  public electronService: ElectronService,
) {
  // previous code...

  if (electronService.isElectronApp) {
    // Do electron stuff
  } else {
    // Do other web stuff
  }

}
Tip
A list of all accesible APIs can be found at Thorsten Hans' ngx-electron repository.
Create the electron window in main.ts

In order to use electron, a file needs to be created at the root of the application (main.ts). This file will create a window with different settings checking if we are using --serve as an argument:

import { app, BrowserWindow, screen } from 'electron';
import * as path from 'path';
import * as url from 'url';

let win: any;
let serve: any;
const args: any = process.argv.slice(1);
serve = args.some((val) => val === '--serve');

 function createWindow(): void {
  const electronScreen: any = screen;
  const size: any = electronScreen.getPrimaryDisplay().workAreaSize;

   // Create the browser window.
  win = new BrowserWindow({
    x: 0,
    y: 0,
    width: size.width,
    height: size.height,

    // Needed if you are using service workers
    webPreferences: {
      nodeIntegration: true,
      nodeIntegrationInWorker: true,
    }
  });

   if (serve) {
    // tslint:disable-next-line:no-require-imports
    require('electron-reload')(__dirname, {
      electron: require(`${__dirname}/node_modules/electron`),
    });
    win.loadURL('http://localhost:4200');
  } else {
    win.loadURL(
      url.format({
        pathname: path.join(__dirname, 'dist/index.html'),
        protocol: 'file',
        slashes: true,
      }),
    );
  }

   // Uncoment the following line if you want to open the DevTools by default
  // win.webContents.openDevTools();

   // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store window
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    // tslint:disable-next-line:no-null-keyword
    win = null;
  });
}

 try {
  // This method will be called when Electron has finished
  // initialization and is ready to create browser windows.
  // Some APIs can only be used after this event occurs.
  app.on('ready', createWindow);

   // Quit when all windows are closed.
  app.on('window-all-closed', () => {
    // On OS X it is common for applications and their menu bar
    // to stay active until the user quits explicitly with Cmd + Q
    if (process.platform !== 'darwin') {
      app.quit();
    }
  });

   app.on('activate', () => {
    // On OS X it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (win === null) {
      createWindow();
    }
  });
} catch (e) {
  // Catch Error
  // throw e;
}
Add the electron window and improve the package.json scripts

Inside package.json the electron window that will be transformed to main.js when building needs to be added.

{
  ....
  "main": "main.js",
  "scripts": {
  ....
}

The scripts section in the package.json can be improved to avoid running too verbose commands. As a very complete example we can take a look to the My Thai Star’s scripts section and copy the lines useful in your project.

  "scripts": {
    "postinstall": "npx electron-builder install-app-deps",
    ".": "sh .angular-gui/.runner.sh",
    "ng": "ng",
    "start": "ng serve --proxy-config proxy.conf.json -o",
    "start:electron": "npm-run-all -p serve electron:serve",
    "compodoc": "compodoc -p src/tsconfig.app.json -s",
    "test": "ng test --browsers Chrome",
    "test:ci": "ng test --browsers ChromeHeadless --watch=false",
    "test:firefox": "ng test --browsers Firefox",
    "test:ci:firefox": "ng test --browsers FirefoxHeadless --watch=false",
    "test:firefox-dev": "ng test --browsers FirefoxDeveloper",
    "test:ci:firefox-dev": "ng test --browsers FirefoxDeveloperHeadless --watch=false",
    "test:electron": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "ngsw-config": "npx ngsw-config dist ngsw-config.json",
    "ngsw-copy": "cp node_modules/@angular/service-worker/ngsw-worker.js dist/",
    "serve": "ng serve",
    "serve:open": "npm run start",
    "serve:pwa": "npm run build:pwa && http-server dist -p 8080",
    "serve:prod": "ng serve --open --prod",
    "serve:prodcompose": "ng serve --open --configuration=prodcompose",
    "serve:node": "ng serve --open --configuration=node",
    "build": "ng build",
    "build:pwa": "ng build --configuration=pwa --prod --build-optimizer && npm run ngsw-config && npm run ngsw-copy",
    "build:prod": "ng build --prod --build-optimizer",
    "build:prodcompose": "ng build --configuration=prodcompose ",
    "build:electron": "npm run electron:serve-tsc && ng build --base-href ./",
    "build:electron:dev": "npm run build:electron -- -c dev",
    "build:electron:prod": "npm run build:electron -- -c production",
    "electron:start": "npm-run-all -p serve electron:serve",
    "electron:serve-tsc": "tsc -p tsconfig-serve.json",
    "electron:serve": "wait-on http-get://localhost:4200/ && npm run electron:serve-tsc && electron . --serve",
    "electron:local": "npm run build:electron:prod && electron .",
    "electron:linux": "npm run build:electron:prod && npx electron-builder build --linux",
    "electron:windows": "npm run build:electron:prod && npx electron-builder build --windows",
    "electron:mac": "npm run build:electron:prod && npx electron-builder build --mac"
  },

Here the important thing to look out for is that the base href when building electron can be changed as needed. In our case:

    "build:electron": "npm run postinstall:electron && npm run electron:serve-tsc && ng build --base-href \"\" ",
Note
Some of these lines are intended to be shortcuts used in other scripts. Do not hesitate to modify them depending on your needs.

Some usage examples:

$ npm run electron:start                # Serve Angular app and run it inside electron
$ npm run electron:local                # Serve Angular app for production and run it inside electron
$ npm run electron:windows              # Build Angular app for production and package it for Windows OS
$ yarn run electron:start                # Serve Angular app and run it inside electron
$ yarn run electron:local                # Serve Angular app for production and run it inside electron
$ yarn run electron:windows              # Build Angular app for production and package it for Windows OS

20.3. Angular Mock Service

We’ve all been there: A new idea comes, let’s quickly prototype it. But wait, there’s no backend. What can we do?

Below you will find o solution that will get your started quick and easy. The idea is to write a simple mock service that helps us by feeding data into our components.

20.3.1. The app we start with

Let’s say you have a simple boilerplate code, with your favorite styling library hooked up and you’re ready to go. The Angular Material sample is a good starting place.

20.3.2. The Components

Components - are the building blocks of our application. Their main role is to enable fragments of user interfaces. They will either display data (a list, a table, a chart, etc.), or 'collect' user interaction (e.g: a form, a menu, etc.)

Components stay at the forefront of the application. They should also be reusable (as much as possible). Reusability is key for what we are trying to achieve - a stable, maintainable frontend where multiple people can contribute and collaborate.

In our project, we are at the beginning. That means we may have more ideas than plans. We are exploring possibilites. In order to code eficiently:
1) We will not store mock data in the components.
2) We will not fetch or save data directly in the components.

20.3.3. The Service

So, how do we get data in our app? How do we propagate the data to the components and how can we send user interaction from the components to the our data "manager" logic.

The answer to all these questions is an Angular Service (that we will just call a service from now on).

A service is an injectable logic that can be consumed by all the components that need it. It can carry manipulation functions and ,in our case, fetch data from a provider.

Service Architecture
Figure 84. Angular Components & Services architecture.

Inside the Angular App, an Injector gives access to each component to their required services. It’s good coding practice to use a distinct service to each data type you want to manipulate. The type is described in a interface.

Still, our ideas drive in diferent ways, so we have to stay flexible. We cannot use a database at the moment, but we want a way to represent data on screen, which can grow organically.

20.3.4. The Model

Data Box
Figure 85. Data box in relation to services and components.

Let’s consider a 'box of data' represented in JSON. Phisicly this means a folder with some JSON/TS files in it. They are located in the app/mock folder. The example uses only one mock data file. The file is typed according to our data model.

Pro tip: separate your files based on purpose. In your source code, put the mock files in the mock folder, components in the components folder, services in the services folder and data models in the models folder.

Project Structure
Figure 86. Project structure.

Aligned with the Angular way of development, we are implementing a model-view-controler pattern.

The model is represented by the interfaces we make. These interfaces describe the data structures we will use in our application. In this example, there is one data model, coresponding with the 'type' of data that was mocked. In the models folder you will find the .ts script file that describes chemical elements. The corresponding mock file defines a set is chemical emlements objects, in accordance to our interface definition.

20.3.5. Use case

Enough with the theory, let’s see what we have here. The app presents 3 pages as follows:

  • A leader bord with the top 3 elements

  • A data table with all the elements

  • A details page that reads a route paramenter and displays the details of the element.

There are a lot of business cases which have these requirements:

  • A leader board can be understood as "the most popular items in a set", "the latest updated items", "you favorite items" etc.

  • A data table with CRUD operations is very useful (in our case we only view details or delete an item, but they illustrate two important things: the details view shows how to navigate and consume a parametric route, the delete action shows how to invoke service operations over the loaded data - this means that the component is reusable and when the data comes with and API, only the service will need it’s implementation changed)

Check out the Angular Mock Service sample from the samples folder and easily get started with fast data roundtrips between your mock data and your components.

Last updated 2019-12-11 12:58:44 UTC