Chapter 19. Creating Custom Widgets

Developing Custom Widgets is the way to go to quickly customize a the web client. It allows to provide widgets that will be configured and used in one or more Views or Dashboards. The generated projects contain a sample widget that is mentioned in this documentation, under the following folder:

my-project/web/src/app/modules/sample-widget

DOC widgets are developed using Angular framework and Clarity HTML/CSS framework. If you are not familiar with Angular development here are some useful links:

DOC allows you to configure your application by creating and organizing Dashboard and Custom Views. Dashboard and Custom Views are configured to use Custom Widgets.

DOC Web Client Library makes easy developing and integrating a Custom Widget by providing a set of APIs. For more details, refer to Section Understanding the Web Client Library APIs.

The following diagram exposes the available Interfaces and Classes to implement Custom Widgets.

Figure 19.1. Understanding Interfaces and Classes
Understanding Interfaces and Classes

This documentation examples show how to address different use cases with DOC APIs implementing a sample widget. The following topics are mentioned with this sample widget example.

  • Create a Custom Widget using Angular Component.

  • Packaging an Angular Module with a Custom Widget.

  • Custom Widget configurations.

  • Custom Widget model and data loading.

  • Usage Scenario API to fetch and store scenario metadata.

  • Usage of Data API to query scenario data.

  • Available DOC APIs.

In any case, to create a Custom Widget, you need a few things:

  • One Angular Component that implements GeneWidget Interface

  • One Manifest describing the Custom Widget

  • Package and Register the component through an Angular Module

1. Creating the Angular Component

The minimal requirement for any Angular Component to be integrated in DOC as a Custom Widget, is to implement GeneWidget interface.

/**
 * Copyright (c) 2019 DecisionBrain SAS. All Rights Reserved.
 *
 * Base and minimal interface a DOC Widget must implement to be integrable in custom views and dashboards.
 *
 * This interface does not define any method but is used to enforce component Type at module compilation time.
 */
export interface GeneWidget {
}

For example; we can create a very simple component that will be integrable in DOC Views and Dashboards, as follows:

/**
 * Copyright (c) DecisionBrain SAS. All Rights Reserved.
 *
 * This is a DOC Sample Widget
 */
@Component({
    selector: 'sample-widget',
    templateUrl: './map-widget.component.html'
})
export class SampleWidgetComponent implements GeneWidget {

    constructor() {
    }
}

2. Declaring the GeneWidgetManifest

To integrate a Custom Widget in DOC, you have to define a Manifest object that will describe and expose all your widget characteristics.

/**
 * Copyright (c) 2019 DecisionBrain SAS. All Rights Reserved.
 *
 * GeneWidgetManifest is a descriptor class of components that can be used as
 * in Dashboards or Custom Views.
 *
 * Any Custom Widget needs a manifest published by its module in the CustomDashboardWidgetFactoryService
 * and also requires to be declared as 'entryComponent' of its module.
 */
export interface GeneWidgetManifest {

    /**
     * Widget name that will be used in the web client
     */
    name: string;

    /**
     * Widget Version
     */
    version?: string;

    /**
     * Widget Description, basically what it does, how it visualize data
     */
    description: string;

    /**
     * DOC widget simple className (case-sensitive)
     */
    widgetTypeName: string;


    /**
     * DOC Widget configurator className (case-sensitive). The configurator
     * is the component used to configure the widget within Dashboard or Custom Views
     */
    configuratorTypeName?: string;

    /**
     * This flag is set to true when the Widget can be used in a Dashboard
     */
    dashboardCompatible: boolean;

    /**
     * This flag is set to true when the widget can be used in Custom Views
     */
    customViewCompatible: boolean;
	

    /**
     * Dashboard minimum layout columns used by the Widget (a Dashboard has twenty (20) columns by default)
     */
    minItemCols?: number;

    /**
     * Dashboard minimum layout rows used by the Widget (a Dashboard has twenty (20) rows by default)
     */
    minItemRows?: number;


    /**
     * Dashboard default layout rows used by the widget
     */
    itemRows?: number;


    /**
     * Dashboard default layout columns used by the widget
     */
    itemCols?: number;

    /**
     * When this property is set to true, the component will not inherit from Gene
     * widget default css (borders, colors).
     *
     * Use it, for example, if you do not want a component to get a card style when
     * placed in a dashboard.
     */
    preventDefaultCss?: boolean;

    /**
     * Set this property to true to use the default CSS but without a border and with a white background.
     *
     * This is useful if the overflow of the widget needs to be contained, but without showing borders (e.g. Rich Text)
     */
    defaultCssNoBorder?: boolean;
}

For example:

    public static MANIFEST: GeneWidgetManifest = {
        name: 'Sample Widget',
        version: '0.1.0',
        description: 'This widget is a DOC Sample Widget',
        widgetTypeName: 'SampleWidgetComponent',
        configuratorTypeName: 'SampleWidgetConfiguratorComponent',
        dashboardCompatible: true,
        customViewCompatible: true,

        minItemCols : 2,
        minItemRows : 2,
        itemRows : 3,
        itemCols : 3,
    };

3. Packaging the Custom Widget in an Angular Module

The last part required to make a Widget available in the web client is to expose and register it through an Angular module.

3.1. Declaring the Custom Widget

The Widget (and its configurator if any) has to be declared in an Angular module:

  • In Section "declarations" of the module (as any other Angular component).

  • In Section "entryComponents" of the module to allow its usage in custom dashboards and views.

3.2. Registering the Custom Widget

An Angular application may load different modules, sometime lazy loaded, depending on the project size, organization or complexity.

Each module that registers Custom widgets needs to register the widget and its (optional) configurator widget through the GeneCustomWidgetFactoryService.

The Custom Widget sample below shows how to do it.

@NgModule({
    declarations : [SampleWidgetComponent, SampleWidgetConfiguratorComponent],
    entryComponents : [SampleWidgetComponent, SampleWidgetConfiguratorComponent],
    providers: [SampleWidgetService],
    imports : [
        CommonModule,
        FormsModule,
        ReactiveFormsModule,
        ClrInputModule,

        // DOC Modules dependencies
        GeneCommonUiModule,
        GeneLoadingOverlayModul

    ]
})
export class SampleWidgetModule {

    constructor(customWidgetFactory: GeneCustomWidgetFactoryService) {

        // Register my component Manifest into the custom Widget factory, so
        // it is accessible through the wizard
        customWidgetFactory.registerWidget(SampleWidgetComponent.MANIFEST, SampleWidgetComponent, SampleWidgetConfiguratorComponent);
    }
}

4. Implementing Features in a Custom Widget

To improve our Simple Widget, we will be able to associate configurations so that our Simple Widget will be configurable and usable in different Views and Dashboards.

Figure 19.2. Configuring a Dashboard/View
Configuring a Dashboard/View
Figure 19.3. Editing a Widget Configuration
Editing a Widget Configuration
Figure 19.4. Configuring a Widget
Configuring a Widget

Configurations may store anything useful for your component. Configurations are JSON stored objects, so they do not contain any code. In an application any instance of a Custom Widget will have a different configuration instance associated. Configurations usually come with a Configurator Component that is an Angular Component and that will be used by the framework to let the user configure different instances of the Custom Widget.

Implementing the GeneBaseSimpleWidget will enable configuration support. DOC automatically handles the persistence and the injection of these configurations in your application.

The Sample Widget component uses a Configuration type that allows the user to edit its title. A configurator class that allows the user to edit this title is provided as a sample source code.

4.1. Implementing a Custom Widget Configurator

/**
 * Copyright (c) DecisionBrain SAS. All Rights Reserved.
 *
 * This is the base interface of dynamic widget parameters.
 */
 export interface GeneWidgetConfiguration {}

For example, we will make the "title" of the Sample Widget configurable.

/**
 * Sample Widget configuration class
 */
export interface SampleWidgetConfiguration extends GeneWidgetConfiguration {
    // Current widget instance title
    title?:string;
}

The configurator component will integrate with DOC by dispatching GeneWidgetConfigurationEvent events each time the user modify the configuration. The configurator is responsible for providing validation status of the edited configuration through those events. DOC will not let the user save a Widget configuration that is invalid. See sample-widget-configurator.component.ts in the sample widget source code.


/**
 * Copyright (c) DecisionBrain SAS. All Rights Reserved.
 *
 * This interface describes a component that can be instantiated dynamically (e.g. custom dashboard) and
 * initialized through a context object.
 *
 * By implementing this interface you make sure that a component is Custom Dashboard "ready"
 */
export interface GeneWidgetConfigurationAware<T extends GeneWidgetConfiguration> extends GeneWidgetContextAware {
    ...
}


/**
 * Copyright (c) DecisionBrain SAS. All Rights Reserved.
 *
 * Event class, describes the events triggered by a DynamicWidgetConfigurator each time
 * the context is modified by the user
 */
export class GeneWidgetConfigurationEvent<T extends GeneWidgetConfiguration> {

    /**
     * The current configured context
     */
    public configuration: T;

    /**
     * Event type
     */
    public type: GeneWidgetConfigurationEventType;

    /**
     * This object contains information about configuration validity and
     * potential error message if not valid.
     */
    public validationResult?: ValidationResult;
}

/**
 * Copyright (c) DecisionBrain SAS. All Rights Reserved.
 *
 * Type of events for GeneWidgetConfigurationEvent
 */
export enum GeneWidgetConfigurationEventType {
    CONFIGURATION_CHANGED
}

4.2. Managing a Custom Widget State

Enabling GeneWidgetStateAware allows you to save and restore some state properties of a widget. For more details, refer to Section Understanding Widget State.

GeneBaseDataWidgetComponent provides a base implementation of the Widget state management, and when using this class as the base class of a Widget the only methods that need to be overridden are:

restoreWidgetState(state: T);

getDefaultWidgetState(): T;

The provided Sample Map Widget illustrates how to use this API to save and restore the map position and zoom level.

/**
     * Method that is being called when the framework injects a widget state.
     *
     * This method restore the Map center from the saved stated.
     * @param widgetState
     */
    applyWidgetState(widgetState:SampleMapState) {
        if(widgetState) {
            this.center = {lat: widgetState.lat, lng: widgetState.lng};
            this.zoom = widgetState.zoom;
        }
    }

    /**
     * When the Map center or zoom change, this code saves the Widget state
     * through DOC API updateWidgetState(S) method.
     */
    onMapChanged() {
        const center = this.googleMap.getCenter();
        const zoom = this.googleMap.getZoom();
        this.updateWidgetState( { lat: center.lat(), lng: center.lng(), zoom: zoom});
    }

    getDefaultWidgetState(): SampleMapState {
        return {
            zoom: 5,
            lat: NYC_COORDINATES.lat,
            lng: NYC_COORDINATES.lng,
        }
    }

4.3. Loading Data into a Custom Widget

The base class GeneBaseDataWidget provides additional features like data loading mechanisms and event handlers.

Widgets inheriting this base class need to implement loadData() method that will be called automatically by DOC framework. Also if your component needs to use Angular OnInit life cycle hook, you will have to call super.ngOnInit(); in the ngOnInit() method so that GeneBaseDataWidget will be initialized properly.

Note that loadData() is automatically called by the framework and should never be called manually, if your widget needs to force data refresh, then you should call the requestLoadData() method.

This class also provides base methods you can override to integrate with the Framework Events. You can see Angular documentation for full details about how to integrate with DOC different events and life cycle.


    /**
     * Method called when the DynamicWidgetConfiguration is set.
     * You should configure your widget from widgetParameters by overriding this method
     * @param widgetParameters : widgetParameters that should be used to configure the state of the widget
     */
    protected applyWidgetConfiguration(widgetParameters: T) {
    }

    /**
     * This method is called when a GeneTaskEvent occurs.
     * Override it to implement custom behaviors
     */
    protected onGeneTaskEvent(event: GeneTaskEvent) {
    }

    /**
     * This method is called when a GeneScenarioEvent occurs.
     * Override it to implement custom behaviors
     */
    protected onGeneScenarioEvent(event: GeneScenarioEvent) {
    }

    /**
     * This method is called when a GeneUIEvent occurs.
     * Override it to implement custom behaviors
     */
    protected onGeneUIEvent(event: GeneUIEvent) {
    }

    /**
     * This method is called when a GeneDataEvent occurs.
     * Override it to implement custom behaviors
     * @param event
     */
    protected onGeneDataEvent(event: GeneDataEvent) {
    }


    /**
     * Handler automatically called when a GeneContextEvent of type HIGHLIGHT_CHANGED is received.
     * Override this method to handle those events in your widget implementation.
     *
     * @param event GeneContextEvent of type HIGHLIGHT_CHANGED
     */
    protected onContextHighlightChanged(event: GeneContextEvent) {
    }

    /**
     * Handler automatically called when a GeneContextEvent of type SCENARIO_CHANGED is received.
     *
     * Its base implementation calls the requestLoadData() method.
     *
     * Override this method to handle those events in your widget implementation.
     *
     * @param event GeneContextEvent of type SCENARIO_CHANGED
     */
    protected onContextScenarioChanged(event: GeneContextEvent) {
    }

    /**
     * Handler automatically called when a GeneContextEvent of type SELECTION_CHANGED is received.
     * Override this method to handle those events in your widget implementation.
     *
     * @param event GeneContextEvent of type SELECTION_CHANGED
     */
    protected onContextSelectionChanged(event: GeneContextEvent) {
    }

    /**
     * Handler automatically called when a GeneContextEvent of type FILTER_CHANGED is received.
     * Override this method to handle those events in your widget implementation.
     *
     * @param event GeneContextEvent of type FILTER_CHANGED
     */
    protected onContextFilterChanged(event: GeneContextEvent) {
    }

    /**
     * Handler automatically called each time data have been loaded.
     * @param data the loaded data
     */
    protected onDataLoaded(data: D) {
    }

    /**
     * Handler automatically called each time requestLoadData() is called but cannot be
     * executed because canLoadData() returned false.
     * @param data the loaded data
     */
    protected onCanNotLoadData() {
    }

    /**
     * Handler automatically called each time loadData() failed to load data.
     * @param error the data loading error
     */
    protected onLoadDataError(error: any) {
    }


    /**
     * This method should return either an observable emitting a single value or a boolean.
     *
     * Returning a true value means the widget framework can safely call loadData(), all
     * the required initializations are completed.
     *
     * Returning false means the widget is not ready to load data yet, and this method
     * will be called again upon #requestLoadData() next call.
     *
     * The default implementation checks that widgetConfiguration and context are set
     * and, in comparison mode, that the involved scenarios have a VALID data status.
     *
     * Override this method if you implement additional checks.
     */
    protected canLoadData(): boolean | Observable<boolean> {
    }

The Sample Widget integrated with Configuration and Data Loading mechanism:

@Component({
    selector : 'app-sample-widget',
    templateUrl : './sample-widget.component.html',
    styleUrls : ['./sample-widget.component.scss']
})
export class SampleWidgetComponent extends GeneBaseDataWidgetComponent<SampleWidgetConfiguration, SampleWidgetModel> {

    // Widget Manifest used
    public static MANIFEST: GeneWidgetManifest = {
        widgetTypeName : 'SampleWidgetComponent',
        configuratorTypeName: 'SampleWidgetConfiguratorComponent',
        version : '1.0',
        description : 'Simple Demo Sample Widget',
        name : 'Sample Widget',
        customViewCompatible : true,
        dashboardCompatible : true,
        minItemRows : 5,
        minItemCols : 5,
        itemRows : 5,
        itemCols : 5
    };

    constructor(protected injector: Injector,
                private service: SampleWidgetService,
                private toastrService: ToastrService) {
        super(injector);
    }

    // Called by the framework when required
    loadData(context: GeneContext): Observable<SampleWidgetModel> {
        return this.service.loadWidgetModel(context);
    }

    // Event handler when a user clicks on validate or invalidate
    onUpdateStatus(status: SampleScenarioStatus) {
        this.service.updateScenarioStatus(this._context, status)
            .pipe(
                tap( () => this.requestLoadData() )
            )
            .subscribe(
            result => this.toastrService.info('Scenario Updated with Status ' + status),
            error => this.toastrService.error('Error While Updating Status ' + error)
        );
    }
}

}