Integrating with Angular

Note

For the purposes of this guide, the following assumes your extension is named AccountDashboard.

Angular is a great tool for making rich user interfaces for the Caliper SDK, but there is some setup work to be done. First, install @angular/cli and bootstrap a new Angular application:

$ cd AccountDashboard/frontend
$ yarn add @angular/cli
$ yarn run ng new account-dashboard --directory . --force --routing --package-manager=yarn

Now install Tecton, the front-end library developed for the Caliper SDK:

$ yarn add q2-tecton-sdk

Note

If using Tecton before v0.42.2 or v1.1.0, you will need to downgrade zone.js like so: yarn add zone.js@~0.9.1

Let’s generate some files that will be needed later:

$ yarn run ng generate service tecton
$ yarn run ng generate service store

Update the output path to coordinate with SDK expectations:

$ ng config projects.account-dashboard.architect.build.options.outputPath dist/

Start up your editor or IDE and open AccountDashboard/frontend/angular.json and add a baseHref key with a value of . to the build options:

{
  // ...
  "projects": {
    // ...
    "architect": {
      "build": {
        // ...
        "options": {
          // ...
          "baseHref": "."
        }
      }
    }
  }
}

Add Tecton to the “BROWSER POLYFILLS” section of src/polyfills.ts before the zone.js import:

/**
 * ...
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

import "q2-tecton-sdk";

/**
 * ...
 */

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import "zone.js/dist/zone"; // Included with Angular CLI.

/***************************************************************************************************
 * APPLICATION IMPORTS
 */

Fill in the new frontend/src/app/tecton.service.ts with the following. This adds Tecton capability to your entire Angular app:

import { Injectable } from "@angular/core";
import { connect } from "q2-tecton-sdk";

@Injectable({
  providedIn: "root"
})
export class TectonService {
  actions: any;
  sources: any;
  async init() {
    const { actions, sources } = await connect();

    this.actions = actions;
    this.sources = sources;
  }
}

Fill in the new frontend/src/app/store.service.ts service with the following. This is the place to define methods that interface with the server of our extension:

import { Injectable } from "@angular/core";
import { TectonService } from "./tecton.service";

@Injectable({
  providedIn: "root"
})
export class StoreService {
  constructor(private q2: TectonService) {}

  async getData() {
    // Request data from our extension's `default` route
    const response = await this.q2.sources.requestExtensionData({
      route: "default"
    });

    return response.data;
  }
}

Edit frontend/src/app/app.module.ts and add the initTecton app initializer:

import { BrowserModule } from "@angular/platform-browser";
import { NgModule, APP_INITIALIZER } from "@angular/core";

import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { TectonService } from "./tecton.service";

// This function initializes Tecton from our `TectonService`
function initTecton(tectonService: TectonService) {
  return () => tectonService.init();
}

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule],
  providers: [
    {
      provide: APP_INITIALIZER, // Run this factory on app init
      useFactory: initTecton, // Call our Tecton init factory
      deps: [TectonService], // Our factory depends on the `TectonService`
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Make frontend/src/app/app.component.ts load and display our data fetched from the server (see inline comments for details).

import { Component, OnInit } from "@angular/core";
import { StoreService } from "./store.service";
import { TectonService } from "./tecton.service";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
  // Initialize our `data` property with a default value
  // which we will display in our template.
  data = "Loading";

  // Inject both `TectonService` and `StoreService`
  constructor(private q2: TectonService, private storeService: StoreService) {}

  async ngOnInit() {
    // Fetch our data from our `default` route using our store service
    this.data = JSON.stringify(await this.storeService.getData());
    // Hide the loading spinner once our data has loaded
    await this.q2.actions.setFetching(false);
  }
}

Open frontend/src/app/app-routing.module.ts and add the options object with a useHash key like so:

import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes, { useHash: true })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Finally, edit frontend/src/app.component.html to display the retrieved data. Here, we are simply displaying the stringified JSON object returned from the server.

{{ data }}

You should now be able to go back to your project’s root directory and run your extension:

$ cd ../..
$ q2 run