Cumulocity Web Development Tutorial - Part 2: Create your first component

Welcome back!

welcome-back

Welcome back to the second part of the Cumulocity Web Development Tutorial series. In the first part, you have started to set up your development environment by installing Node.js and the Cumulocity Web Developer command line tools (@c8y/cli). Using these tools, you have created your first application, which doesn’t have any content yet. This is going to change.

In the second part you will create your first component and add a new entry in the navigation menu to access this component. The component will be fairly simple. It will display some hardcoded device details and a random temperature measurement.

image

Even though the component is simple, you will learn quite some new concepts and best practices. You will learn how you can hook into built-in Cumulocity components, such as the navigation menu to extend it with custom entries. Furthermore, you will get to know Cumulocity’s styleguide to build UIs, which follow Cumulocity’s look and feel. In addition, there will be an introduction to content projection in the context of Cumulocity.

Starting simple

The component you will implement, registers a new entry in the navigation menu on the left side. Clicking on the navigation entry will open a detail page, which displays general information and temperature measurements for a device. For now, the device and its data will be mocked. In the next part of this tutorial series this is going to change. You will display information for an actual device, which is available in your Cumulocity instance.

Let’s start coding. You can find the source code for this article in the corresponding git repository in the part 02 directory.

cat-coding

You can extend the existing application from the first part or create a new application from scratch. Create a new directory in your project called src. The src directory will be the location, where you will place your modules, components and services. Inside the src folder create another folder called device-info. This will be the home to your first module.

Create the model

As you are a exemplary Typescript and Angular developer, you are well aware of separation of concerns (MVC) and providing type definitions. This means, the code will be separated based on the responsibility. Let’s define the model of our component first. Create a new file inside the device-info directory, called device-info.model.ts. It will consist of two interfaces:

device-info.model.ts

export interface DeviceDetails {
  name: string;
  type: string;
}

export interface TemperatureMeasuerement {
  value: number;
  unit: string;
}

The DeviceDetails interface is used to describe the structure of a device. For now, a device is described by its name and type. As the device is capable of sending temperature measurements, there is a second interface to define the structure of such a temperature measurement: TemperatureMeasurement. A temperature measurement consists of a value and a unit.

Create some data

These interfaces will be used in the service of the device-info module to provide data. The service is responsible for creating device data and temperature measurements, which later will be picked up by the component. Next to the device-info.model.ts file create another file called device-info.service.ts.

device-info.service.ts

import { Injectable, resolveForwardRef } from '@angular/core';
import { Subject } from 'rxjs';
import { DeviceDetails, TemperatureMeasuerement } from './device-info.model';

@Injectable()
export class DeviceInfoService {
  temperatureMeasurement$: Subject<TemperatureMeasuerement> =
    new Subject<TemperatureMeasuerement>();

  constructor() {}

  async getDeviceDetails(): Promise<DeviceDetails> {
    return new Promise<DeviceDetails>((resolve) =>
      resolve({ name: 'My test device', type: 'c8y_TestType' })
    );
  }

  subscribeForTemperatureMeasurements(): void {
    // publish latest measurement
    this.temperatureMeasurement$.next({ value: 10, unit: '°C' });

    // push random temperature every 10 seconds
    setInterval(
      () => this.temperatureMeasurement$.next({ value: this.getRandomInt(8, 15), unit: '°C' }),
      10000
    );
  }

  private getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min) + min);
  }
}

Let’s have a look at the service. The service will be the foundation for the third part of this tutorial series, where you will query data using Cumulocity’s API. In this example, the data is mocked. The service contains two public methods:

async getDeviceDetails(): Promise<DeviceDetails> {
    return new Promise<DeviceDetails>((resolve) =>
      resolve({ name: 'My test device', type: 'c8y_TestType' })
    );
}

The getDeviceDetails() method uses the DeviceDetails interface, which you have created previously to return a mocked device as Promise. Furthermore, the service provides a Subject to which a component can subscribe to. The subject is used to publish incoming temperature measurements:

 temperatureMeasurement$: Subject<TemperatureMeasuerement> = new Subject<TemperatureMeasuerement>();

The measurements are created by the second method subscribeForTemperatureMeasurements().

subscribeForTemperatureMeasurements(): void {
    // publish latest measurement
    this.temperatureMeasurement$.next({ value: 10, unit: '°C' });

    // push random temperature every 10 seconds
    setInterval(
      () => this.temperatureMeasurement$.next({ value: this.getRandomInt(8, 15), unit: '°C' }),
      10000
    );
}

The method initially publishes the latest mocked temperature measurement. Afterwards, an interval of 10 seconds is specified to continuously publish random temperature measurements.

Create the component

To display this data on a page, a component is required, which orchestrates the communication between template and service. The component is created in the device-info directory and is called device-info.component.ts. It will use the service, which you created previously, to receive data about the device and its temperature measurements. This data is displayed in the corresponding template, which you will create later.

device-info.component.ts

import { Component, OnInit } from '@angular/core';
import { DeviceDetails, TemperatureMeasuerement } from './device-info.model';
import { DeviceInfoService } from './device-info.service';

@Component({
  selector: 'c8y-device-info',
  templateUrl: 'device-info.component.html',
  providers: [DeviceInfoService],
})
export class DeviceInfoComponent implements OnInit {
  tempteratureMeasurement: TemperatureMeasuerement;

  deviceDetails: DeviceDetails;

  constructor(private deviceInfoService: DeviceInfoService) {}

  ngOnInit() {
    this.initDeviceDetails();
    this.subscribeForTemperatureMeasurements();
  }

  private async initDeviceDetails() {
    this.deviceDetails = await this.deviceInfoService.getDeviceDetails();
  }

  private subscribeForTemperatureMeasurements() {
    this.deviceInfoService.temperatureMeasurement$.subscribe(
      (temperatureMeasurement) => (this.tempteratureMeasurement = temperatureMeasurement)
    );

    this.deviceInfoService.subscribeForTemperatureMeasurements();
  }
}

In the @Component annotation, the component provides the DeviceInfoService, so it can be used by the component. The component has two variables defined:

  tempteratureMeasurement: TemperatureMeasuerement;

  deviceDetails: DeviceDetails;

Both variables use the interfaces, which have been defined in the corresponding model file. They will be used by the template to display relevant data to the user. By implementing the OnInit interface the component registers the lifecycle callback ngOnInit to be notified once the component has been initialized. Once ngOnInit is triggered, the component uses the DeviceInfoService to initialize the deviceDetails and to subscribe for temperature measurements.

private subscribeForTemperatureMeasurements() {
    this.deviceInfoService.temperatureMeasurement$.subscribe(
      (temperatureMeasurement) => (this.tempteratureMeasurement = temperatureMeasurement)
    );

    this.deviceInfoService.subscribeForTemperatureMeasurements();
  }

The subscription is done by subscribing to the Subject exposed by the service. At the same time, the component triggers the publication of temperature measurements by executing the method subscribeForTemperatureMeasurements() of the service.

Display information

The last building block is to display the device information. This is done in the template, which will be created inside the device-info directory. Name the file device-info.component.html:

device-info.template.html

<c8y-title>{{ 'Device info' | translate }}</c8y-title>

<div class="card col-sm-3 p-8" *ngIf="deviceDetails">
  <p class="legend form-block">{{ 'Device details' | translate }}</p>
  <div class="form-group">
    <label>{{ 'Name' | translate }}</label>
    <p class="form-control-static">{{ deviceDetails.name }}</p>
  </div>
  <div class="form-group">
    <label>{{ 'Type' | translate }}</label>
    <p class="form-control-static">{{ deviceDetails.type }}</p>
  </div>
  <p class="legend form-block">{{ 'Measurements' | translate }}</p>
  <div class="form-group">
    <label>{{ 'Temperature' | translate }}</label>
    <p class="form-control-static">
      {{ tempteratureMeasurement?.value ?? '-' }} {{ tempteratureMeasurement?.unit }}
    </p>
  </div>
</div>

You can display information about the device and the temperature by using the variables (deviceDetails and temperatureMeasurement) of the corresponding component in the template. Interpolation allows to display the specific values of the two variables.

You might be wondering where some of the CSS classes are coming from, e.g. card or p-8. Cumulocity provides a Styleguide, which is publicly available:

The styleguide for Cumulocity IoT collects and shares information on how you can create user interfaces by following Cumulocity’s UX/UI principles and best practices. In the section Components you can find an overview of existing components as part of the ngx-components package and how to use these in your template. It describes what type of buttons exist and how to use these. Same with the card component, which is used in the template.
The Utilities sections describes CSS utilities, which you can reuse in your application and plugins. These can be for example predefined classes to set the padding (e.g `p-8) or the margin.

Tip: As you can see the Styleguide is really helpful and full of information. Take your time and have a look at it to get a better understanding of existing principles and components in Cumulocity IoT.

The template for the device-info component uses a built-in Cumulocity component:

<c8y-title>{{ 'Device info' | translate }}</c8y-title>

The c8y-title component is part of the ngx-components package. It is used for content projection. For example, you can configure the title of a page by setting a <c8y-title> in any other component. The content of the tag is then projected to an outlet component, which is placed in the header bar.

There are additional components like the c8y-action-bar-item, which can be used for content projection in Cumulocity. Have a look at the documentation for more information.

Navigating to the component

To actually see the component, which you have just created, you need to register it in Cumulocity’s navigation menu. Create a new file called device-info.factory.ts in the device-info directory.

device-info.factory.ts

import { Injectable } from '@angular/core';
import { NavigatorNode, NavigatorNodeFactory } from '@c8y/ngx-components';

@Injectable()
export class DeviceInfoNavigationFactory implements NavigatorNodeFactory {
  get() {
    return new NavigatorNode({
      label: 'Device Info',
      icon: 'robot',
      path: 'device-info',
      priority: 100,
    });
  }
}

The DeviceInfoNavigationFactory is annotated as @Injectable() and implements the NavigatorNodeFactory interface. The interface asks you to override the get() method, which should return a NavigatorNode.

new NavigatorNode({
      label: 'Device Info',
      icon: 'robot',
      path: 'device-info',
      priority: 100,
    });

The NavigatorNode defines a label and an icon. These are later displayed in the menu:

image

The priority specifies at which position the new navigation entry should be shown within the navigation menu.

Bring everything together in a module

To bring everything together, you need to create a module for your device-info component in the corresponding directory called device-info.module.ts.

device-info.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreModule, HOOK_NAVIGATOR_NODES } from '@c8y/ngx-components';

import { DeviceInfoComponent } from './device-info.component';
import { DeviceInfoNavigationFactory } from './device-info.factory';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'device-info',
    pathMatch: 'full',
  },
  {
    path: 'device-info',
    component: DeviceInfoComponent,
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes), CoreModule],
  exports: [],
  declarations: [DeviceInfoComponent],
  providers: [
    { provide: HOOK_NAVIGATOR_NODES, useClass: DeviceInfoNavigationFactory, multi: true },
  ],
})
export class DeviceInfoModule {}

First, you register the DeviceInfoComponent for the path device-info in the routes variable. If this path is called the application will load the corresponding DeviceInfoComponent. The routes are added to the application via the RouterModule as part of the imports statement of the module. The CoreModule is imported to access components from Cumulocity’s core, for example the <c8y-title> component.

Furthermore, the navigation factory, which you have created previously needs to be registered as well. For Cumulocity to recognize the navigation factory, you have to provide it with the special hook called HOOK_NAVIGATOR_NODES in the providers statement. Besides the HOOK_NAVIGATOR_NODES hook there are a couple of additional hooks, which will be covered later in this tutorial series.

Tip: You can read about the various hooks and the underlying concept in the ngx-components documentation

Run your application

Before you can run your application, you must import your newly created module in the app module (app.module.ts). Add the DeviceInfoModule to list of imported modules of AppModules:

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as ngRouterModule } from '@angular/router';
import { CoreModule, BootstrapComponent, RouterModule } from '@c8y/ngx-components';
import { DeviceInfoModule } from './src/device-info/device-info.module';

@NgModule({
  imports: [
    BrowserAnimationsModule,
    RouterModule.forRoot(),
    ngRouterModule.forRoot([], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    DeviceInfoModule,
  ],
  bootstrap: [BootstrapComponent],
})
export class AppModule {}

Now you can run the application locally by executing the command

npx c8ycli server -u <<C8Y-URL>>

as learned in the first part of this tutorial series. Another approach is to update the start script in package.json and replace {{c8y-instance}} with the URL of your Cumulocity environment. To execute the start script, you must run this command:

npm run start

Your application should now have a little bit more content in it:

Conclusion and what’s next

information-overload

This was quite a lot of information to take in. Take your time to process it. The component, which you have implemented in this article, will be the foundation for the next parts of the tutorial series. In the next article, you will learn how to query data from Cumulocity using its API and the Web SDK. Furthermore, you will convert the device-info from a plain plugin to a real Cumulocity widget to be used on Cumulocity dashboards.

Make sure to not miss the next article, which is scheduled to be released in about two weeks.

3 Likes