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

Cumulocity IoT Web Development Tutorial - Part 1: Start your journey
Cumulocity IoT Web Development Tutorial - Part 2: Create your first component
Cumulocity IoT Web Development Tutorial - Part 3: Query data from Cumulocity IoT
Cumulocity IoT Web Development Tutorial - Part 4: Convert component into a widget
Cumulocity IoT Web Development Tutorial - Part 5: Provide widget as a UI Plugin

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 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 Codex 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. In your project, navigate to the src folder. Inside the src directory 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, WritableSignal, signal } from '@angular/core';
import { DeviceDetails, TemperatureMeasuerement } from './device-info.model';

@Injectable()
export class DeviceInfoService {
  private temperatureMeasurement!: WritableSignal<TemperatureMeasuerement>;

  constructor() {}

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

  subscribeForTemperatureMeasurements(): WritableSignal<TemperatureMeasuerement> {
    // publish latest measurement
    this.temperatureMeasurement = signal({ value: 10, unit: '°C' });

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

    return this.temperatureMeasurement;
  }

  private getRandomInt(min: number, max: number): number {
    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 a Promise. Furthermore, the service provides a WritableSignal to which a component can subscribe to receive notifications if the value changes. The signal is used to publish incoming temperature measurements:

private temperatureMeasurement!: WritableSignal<TemperatureMeasuerement>;

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

 subscribeForTemperatureMeasurements(): WritableSignal<TemperatureMeasuerement> {
    // publish latest measurement
    this.temperatureMeasurement = signal({ value: 10, unit: '°C' });

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

  return this.temperatureMeasurement;
}

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

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, WritableSignal } 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!: WritableSignal<TemperatureMeasuerement | undefined>;

  deviceDetails!: DeviceDetails;

  constructor(private deviceInfoService: DeviceInfoService) {}

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

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

  private subscribeForTemperatureMeasurements() {
    this.tempteratureMeasurement =
      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!: WritableSignal<TemperatureMeasuerement | undefined>;

  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.tempteratureMeasurement =
      this.deviceInfoService.subscribeForTemperatureMeasurements();
  }

The subscription is done by subscribing to the WritableSignal using the subscribeForTemperatureMeasurements method provided by 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>

@if (deviceDetails) {
<div class="card col-sm-3 p-8">
  <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 the temperatureMeasurement signal) 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 Codex, which is publicly available:

The Codex 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 Codex 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.

image

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 {
  private readonly NAVIGATOR_NODE = {
    label: 'Device Info',
    icon: 'robot',
    path: 'device-info',
    priority: 100,
  } as NavigatorNode;

  get() {
    return this.NAVIGATOR_NODE;
  }
}

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. 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, hookNavigator } 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: [hookNavigator(DeviceInfoNavigationFactory)],
})
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 must provide it using the hookNavigator() function in the providers statement. Besides the hookNavigator() function there are a couple of additional hook functions, which will be covered later in this tutorial series.

Tip: You can read about the various hook function and the underlying concept in the ngx-components documentation (see section Multi Provider (MP))

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 {
  CoreModule,
  BootstrapComponent,
  RouterModule,
} from '@c8y/ngx-components';
import { DeviceInfoModule } from '../device-info/device-info.module';

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

Now you can run the application locally by executing the command

ng serve -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.

6 Likes