c8y-widget

star 1

Comprehensive guide for developing custom widgets in Cumulocity, covering prerequisites, dynamic components, and step-by-step examples. Use when building UI widgets.

Cumulocity-IoT By Cumulocity-IoT schedule Updated 5/19/2026

name: c8y-widget description: Comprehensive guide for developing custom widgets in Cumulocity, covering prerequisites, dynamic components, and step-by-step examples. Use when building UI widgets.

This section covers the prerequisites for widget development, explains dynamic components, and provides a step-by-step example of building a simple widget and using it in a dashboard.

Prerequisites

Key concepts essential for widget and dashboard development.

Widgets

Widgets are modular components that display data and interact with other parts of the Cumulocity platform. They can present maps, images, graphs, tables, and other graphical representations of data. Widgets are useful for tracking information (such as alarms, assets, or applications), or for providing maps, quick links, and more in dashboards or reports. Multiple widgets are available in c8y/ngx-components, but you can also create your own custom widgets.

Dashboards

Dashboards are the surfaces where widgets are displayed. There are two types of dashboards:

  • Context dashboard: Resolves its data from the current context (device or group) it is displayed on. This type is usually created by the user.
  • Named-context dashboard: A context dashboard with a name, created programmatically for the device as a child addition.

hookComponent and dynamic components

The hookComponent function allows you to add dynamic components to the UI (such as widgets). To use this function, call hookComponent and provide a component object compatible with DynamicComponentDefinition type.

Dynamic components allow you to display already registered components. This mechanism is used internally in ContextDashboardComponent, but you can also use it directly in the DashboardComponent.

Creating an application and adding a custom widget

The following steps describe how to create a new application with a custom widget.

Initialize an example application

Install the Angular 19 @angular/cli package:

npm install @angular/cli@19 -g

Generate a new Angular 19 application:

ng new <appName> --interactive=false --style=less --ssr=false

Navigate to the application directory:

cd <appName>

Use the ng add command to include @c8y/websdk, and follow the prompts. For this example, select the Cockpit application when prompted to choose which application to add.

ng add @c8y/websdk

Create a custom widget

In this example, the custom widget implementation consists of three elements:

WidgetDemo component class

Responsible for displaying a widget on the dashboard. It has one input for the config object, which can be stored in the database and configured by the widget config.

WidgetConfigDemo component class

Handles changing the configuration of an existing or new widget. You must add a config object, which you can fill with any serializable configuration you want to pass to the widget. To enable widget configuration validation, add the appropriate option to the @Component decorator.

DashboardWidgetDemoModule

Encapsulates the entire widget functionality and registers the widget for use in the application. The most important element of the module is the hookComponent. The hook method is called with an object that implements DynamicComponentDefinition and contains all necessary data, such as the dynamic component ID, view and config classes, and settings.

File: demo-widget.component.ts

import { Component, computed, input, OnInit } from "@angular/core";
import {
  DismissAlertStrategy,
  DynamicComponentAlert,
  DynamicComponentAlertAggregator,
} from "@c8y/ngx-components";
import { WidgetConfig } from "./widget-config.model";

@Component({
  selector: "c8y-widget-demo",
  template: `
    <div class="p-16">
      <h1>Demo Widget</h1>
      <p class="text">{{ displayText() }}</p>
      @if (deviceName()) {
        <small>Device: {{ deviceName() }}</small>
      }
      <div class="m-t-16">
        <button class="btn btn-default btn-sm" (click)="showAlert()">Show alert</button>
      </div>
    </div>
  `,
  styles: [
    `
      .text {
        font-size: 1.5em;
        color: var(--c8y-brand-primary);
      }
    `,
  ],
  standalone: true,
})
export class WidgetDemo implements OnInit {
  readonly config = input<WidgetConfig>();

  /** Computed signal for display text with fallback */
  readonly displayText = computed(
    () => this.config()?.text || "No text configured",
  );

  /** Computed signal for device name */
  readonly deviceName = computed(() => this.config()?.device?.name);

  /** Set by the dashboard framework - used to display alerts on the widget */
  alerts: DynamicComponentAlertAggregator;

  ngOnInit(): void {
    // Enable dismissible alerts for warning type
    this.alerts?.setAlertGroupDismissStrategy(
      "warning",
      DismissAlertStrategy.TEMPORARY,
    );
  }

  showAlert(): void {
    this.alerts?.addAlerts(
      new DynamicComponentAlert({
        type: "warning",
        text: "This is a dismissible demo alert!",
      }),
    );
  }
}

File: demo-widget-config.component.ts

import { AsyncPipe } from "@angular/common";
import {
  Component,
  DestroyRef,
  inject,
  Input,
  OnInit,
  TemplateRef,
  ViewChild,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
  ControlContainer,
  FormBuilder,
  FormControl,
  FormGroup,
  NgForm,
  ReactiveFormsModule,
  Validators,
} from "@angular/forms";
import {
  AlertService,
  DynamicComponent,
  FormGroupComponent,
} from "@c8y/ngx-components";
import { WidgetConfigService } from "@c8y/ngx-components/context-dashboard";
import { BehaviorSubject } from "rxjs";
import { WidgetDemo } from "./demo-widget.component";
import { WidgetConfig } from "./widget-config.model";

@Component({
  selector: "c8y-widget-config-demo",
  template: `
    <div class="form-group">
      <c8y-form-group>
        <label>Text</label>
        <textarea style="width: 100%" [formControl]="formGroup.controls.text"></textarea>
      </c8y-form-group>
    </div>

    <ng-template #widgetPreview>
      <c8y-widget-demo [config]="config$ | async"></c8y-widget-demo>
    </ng-template>
  `,
  viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
  standalone: true,
  imports: [FormGroupComponent, ReactiveFormsModule, WidgetDemo, AsyncPipe],
})
export class WidgetConfigDemo implements DynamicComponent, OnInit {
  /** Configuration passed by the dashboard framework. */
  @Input() config: WidgetConfig = {};

  /** Reactive form group for the widget configuration. */
  formGroup: FormGroup<{ text: FormControl<string | null> }>;

  /** Emits config changes for the preview template. */
  config$ = new BehaviorSubject<WidgetConfig>({});

  private readonly alert = inject(AlertService);
  private readonly widgetConfigService = inject(WidgetConfigService);
  private readonly formBuilder = inject(FormBuilder);
  private readonly form = inject(NgForm);
  private readonly destroyRef = inject(DestroyRef);

  @ViewChild("widgetPreview")
  set preview(template: TemplateRef<any>) {
    this.widgetConfigService.setPreview(template ?? null);
  }

  ngOnInit(): void {
    // Create form with initial values from config
    this.formGroup = this.formBuilder.group({
      text: [this.config?.text || "", Validators.required],
    });

    // Register form with parent NgForm for validation
    this.form.form.addControl("widgetConfig", this.formGroup);

    // Initialize preview
    this.config$.next(this.config);

    // Update preview when form values change
    this.formGroup.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((value) => {
        this.config$.next({ ...this.config, ...value });
      });

    // Register save callback - validates and merges form values into config
    this.widgetConfigService.addOnBeforeSave((config) => {
      if (this.formGroup.invalid) {
        this.alert.warning("Please enter a valid text.");
        return false;
      }
      Object.assign(config, this.formGroup.value);
      return true;
    });
  }
}

File: index.ts (Module definition)

import { DynamicComponentErrorStrategy, hookWidget } from "@c8y/ngx-components";
import { WidgetDemo } from "./demo-widget.component";
import { WidgetConfigDemo } from "./demo-widget-config.component";

export function provideDemoWidget() {
  return [
    hookWidget({
      id: "angular.widget.demo",
      label: "Demo Widget",
      description: "A simple demo widget showing text and device context",
      component: WidgetDemo,
      configComponent: WidgetConfigDemo,
      errorStrategy: DynamicComponentErrorStrategy.OVERLAY_ERROR,
      data: {
        schema: () =>
          import(
            "c8y-schema-loader?interfaceName=WidgetConfig!./widget-config.model"
          ),
        settings: {
          noNewWidgets: false,
        },
      },
    }),
  ];
}

Add these three elements to your application source folder and import the provideDemoWidget() function into your application module providers.

Using your custom widget in the application

A widget can be used in two ways: implicitly within a context dashboard or directly as a dynamic component.

Using a widget in a context dashboard

In this scenario, the dynamic component is already integrated into the context dashboard. You can add your custom widget (named My angular widget or similar based on id/label) from the list of available widgets on the home or context dashboard.

Example Context Dashboard Component:

import { Component } from "@angular/core";
import { CoreModule, Widget } from "@c8y/ngx-components";
import { CommonModule } from "@angular/common";
import { ContextDashboardModule } from "@c8y/ngx-components/context-dashboard";

@Component({
  selector: "tut-widget-guide-dashboard",
  template: `
    <c8y-title>Context dashboard</c8y-title>
    <c8y-context-dashboard
      name="example-widget"
      [defaultWidgets]="defaultWidgets"
      [canDelete]="false"
    ></c8y-context-dashboard>
  `,
  standalone: true,
  imports: [ContextDashboardModule, CoreModule, CommonModule],
})
export class WidgetGuideContextDashboardComponent {
  defaultWidgets: Widget[] = [
    {
      _x: 3,
      _y: 0,
      _width: 6,
      _height: 6,
      componentId: "angular.widget.demo",
      config: {
        text: "This text is configured via the widget settings. Click the edit button to change it!",
      },
      title: "Demo Widget Example1",
      id: "demo_widget_example",
    },
  ];
}

Direct usage of a dynamic component

Dynamic components can be used anywhere in the application, but are most commonly utilized as widgets within a dashboard component.

Example Direct Usage (Standalone Component Dashboard):

import { Component, OnDestroy, ViewChild } from "@angular/core";
import { CoreModule } from "@c8y/ngx-components";
import { CommonModule } from "@angular/common";
import { AssetSelectorModule } from "@c8y/ngx-components/assets-navigator";
import { DatapointSelectorModule } from "@c8y/ngx-components/datapoint-selector";
import { NgForm } from "@angular/forms";
import { Subscription } from "rxjs";

@Component({
  selector: "tut-widget-guide-dashboard",
  template: `
    <c8y-title>Custom dashboard</c8y-title>
    <c8y-action-bar-item [placement]="'right'">
      <button class="btn btn-link" title="{{ 'Toggle freeze' }}" (click)="isFrozen = !isFrozen">
        <i [c8yIcon]="isFrozen ? 'lock' : 'unlock'"></i>
        {{ 'Toggle freeze' }}
      </button>
    </c8y-action-bar-item>
    <c8y-dashboard>
      <!-- Dashboard child with a dynamic component -->
      <c8y-dashboard-child [width]="10" [height]="4">
        <c8y-dashboard-child-title>
          <span>Dynamic component child title</span>
        </c8y-dashboard-child-title>
        <c8y-dashboard-child-action>
          <button
            title="{{ 'Configure this widget' }}"
            type="button"
            (click)="editComponent = !editComponent; (false)"
          >
            <i [c8yIcon]="'cog'"></i>
            {{ !editComponent ? 'Configure this widget' : 'Close configuration' }}
          </button>
        </c8y-dashboard-child-action>
        <div class="card-block">
          <form name="form" #configForm="ngForm">
            <!--            important -->
            <c8y-dynamic-component
              componentId="angular.widget.demo"
              [config]="{ text: 'Hello world' }"
              [mode]="editComponent ? 'config' : 'component'"
            ></c8y-dynamic-component>
            <!--            /important-->
          </form>
        </div>
      </c8y-dashboard-child>
    </c8y-dashboard>
  `,
  standalone: true,
  imports: [
    CommonModule,
    CoreModule,
    AssetSelectorModule,
    DatapointSelectorModule,
  ],
})
export class WidgetGuideDashboardComponent implements OnDestroy {
  isFrozen = false;
  editComponent = false;

  @ViewChild("configForm", { static: false })
  configForm!: NgForm;

  subscription!: Subscription;

  ngAfterViewInit() {
    this.subscription = this.configForm.valueChanges!.subscribe((value) =>
      console.log("Widget config:", value),
    );
  }

  ngOnDestroy() {
    this.subscription?.unsubscribe();
  }
}
Install via CLI
npx skills add https://github.com/Cumulocity-IoT/cumulocity-fleet-intelligence-demos --skill c8y-widget
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
Cumulocity-IoT
Cumulocity-IoT Explore all skills →