C3 AI Documentation Home

Authoring UI Component Types

This document serves as an "SDL Compliance Checklist" to define standards, procedures, and best practices for creating new, functional SDL components that will be merged into your component library. The goal is to ensure that code quality and organization is consistent throughout the SDL framework.

Designing your component

The first step in creating your custom SDL component is writing a "design document". This is where you describe the scope and functionality of the planned component, including its fields, configurations, mixins, and any packages utilized in the component implementation.

As part of this design process you must determine the files that you will have to create. Generally, a UI SDL Component consists of the following:

  1. A C3 Type extending UiSdlComponent,
  2. A .ts implementation file, and
  3. A component renderer Type.

In your component library package, uiComponentLibrary for example, create a folder for your new component in the src > ui > components > sdl directory. In this new folder you will create the component's Type file.

Component Type file

The Type file describes all the configurable properties of your component as its public API:

  • Component settings (functionality).
  • Field settings (data retrieval).
  • Settings management functions (Annotated with @uiSdlActionCreator, @uiSdlReducer, @uiSdlEpic).

This Type will extend UiSdlComponent, which will wrap your UiSdlDataSpec, and will mixin any other Types you decided upon in designing the component (e.g. UiSdlFilterable, UiSdlSearchable).

Your component Type file will also have a dataSpec field, inherited from UiSdlComponent, with the @uiSdlDataSpec annotation above it, defining the field within the Type definition that will hold the data to display in your component.

Below this dataSpec field, add the configurable fields for the component that you scoped out in your design document. This includes non-data-related fields like color, labels, Booleans, themes, legend configurations, and generally anything that is not populated by the retrieval of data. Make sure to set default values for fields that should not be null.

If there's a need to define extra fields for internal purposes, those fields should be private.

DataSpec file

The DataSpec file contains the data-specific settings for the component (i.e. field settings). It takes care of data source management (e.g. request building, data retrieval) behind th scenes so that the component can focus on presentation.

Define the DataSpec file in the same folder as the .c3typ file. In this file you will extend one of two DataSpec Types:

For UiSdlFieldBasedDataSpec, define each field that you want to fetch data from as an instance of the UiSdlFieldBasedDataSpecSetting Type. This Type allows you to specify the fieldName of the object that will populate the SDL component.

For UiSdlMetricBasedDataSpec, add a field of Type UiSdlMetricBasedDataSpecSetting, which will define the metric to evaluate and other configurations (e.g. bindings, unit, and entityId).

Both the UiSdlFieldBasedDataSpecSetting and UiSdlMetricBasedDataSpecSetting Types can be extended to support custom configurations for different components.

Data Transforms

If necessary you can define a field, dataTransforms, which will contain an array of transform functions that will be applied after the data is received from your dataSpec but before saving it in the state. You will need to create the custom data transform type and its implementation. The data transform type can then be applied in the component's json file or in the data spec type directly.

Example use cases of data transforms:

  • Converting decimal numbers to percentages, creating a transform that multiplies decimal numbers by 100 to make percentages
  • Getting rid of significant digits to only display whole number
  • Display only the date portion of a DateTime object, getting rid of hour/minute/second
  • Gives human readable text to enum fields
  • Change/shape the format of the raw data to a different format (array, tuples, etc)

In the following example, we would like to utilize data transforms to make specific transformations for each column of a grid:

  • We create a new UiSdlTransformDemoGrid.c3typ to represent the data transform type.

    TransformDemoGrid.c3typ

    Text
    /**
    * Transforms each object in FetchResult for a data grid with appropriate formats for each column
    */
    @typeScript
    type TransformDemoGrid mixes UiSdlDataTransform<FetchResult<SDLDemoShipment>, FetchResult<SDLDemoShipment>>, Value {
    
      /**
      * Main function for the transform, gets raw FetchResult and returns formatted FetchResult
      */
      transform: ~ ts-client
    }
    
  • We then create the implementation file for TransformDemoGrid. Here we implement the transform function which contains the transformations logic that we want. The transform function takes in data argument which is of FetchResult type.

    TransformDemoGrid.ts

    TypeScript
    import { FetchResult, SDLDemoShipment } from '@c3/types';
    import { UiSdlReduxState } from '@c3/types';
    import { getConfigFromState } from '@c3/ui/UiSdlConnected';
    
    /* eslint-disable import/prefer-default-export */
    
    const enumMapping = {
      0: 'Small',
      1: 'Medium',
      2: 'Large',
    };
    
    export function transform(data: FetchResult<SDLDemoShipment>): FetchResult<SDLDemoShipment> {
      data.objs &&
        data.objs.forEach((obj) => {
          // Get rid of significant digits, display only the whole number
          obj.totalPurchasePrice = obj.totalPurchasePrice && parseInt(obj.totalPurchasePrice);
    
          // Display only the date portion of a DateTime object, getting rid of hour/minute/second
          obj.date = obj.date?.toString().split('T')[0];
    
          // Change enum to human readable text
          obj.size = enumMapping[obj.size];
        });
      return data;
    }
  • The data transform type is now ready to be used in a component's json file. Note: User can define multiple data transforms for a component instance. In this case, the result from the transformation defined earlier in the list will be piped to the next transformations defined later in the list.

    SDLDemo.Grid.json

    Text
    {
      "type" : "UiSdlConnected<UiSdlDataGrid>",
      "component" : {
        "dataSpec" : {
          "dataType" : "SDLDemoShipment",
          "dataTransforms" : [ "TransformDemoGrid" ],
          "actionName" : "fetch",
          "columnFields" : [ {
            "fieldName" : "id",
            "label" : "SDLDemoShipment.shipmentId",
            "sortable" : true
          }, {
            "fieldName" : "description",
            "label" : "SDLDemoShipment.description",
            "sortable" : true
          }, {
            "fieldName" : "numberOfUnits",
            "label" : "SDLDemoShipment.numberOfUnits",
            "sortable" : true
          }, {
            "fieldName" : "totalPurchasePrice",
            "label" : "SDLDemoShipment.totalPurchasePrice",
            "sortable" : true
          }, {
            "fieldName" : "date",
            "label" : "SDLDemoShipment.date",
            "sortable" : true
          }, {
            "fieldName" : "destination.id",
            "label" : "SDLDemoShipment.destination",
            "sortable" : true
          } ]
        }
      }
    }
  • Another way to apply the data transform is directly in the data spec c3typ. However, the data transforms defined in the data spec type will be overridden by the data transforms defined in the JSON for a component instance. UiSdlDataGridDataSpec.c3typ

    Text
    @typeScript
    type UiSdlDataGridDataSpec mixes UiSdlBaseDataGridDataSpec {
      ...
    
      /**
      * Array of data transform types.
      */
      dataTransforms: ~ = ['TransformDemoGrid', 'AnotherTransform']
    
      ...
    }

Actions, Reducers and Epics

Redux "actions" are used to change a component's state. Actions describe what needs to change. They are usually triggered by user interaction with the UI (e.g. filter panel submit). An action can trigger a reducer, which carries out the action and often results in a modified state. An action can also trigger an epic, which executes code asynchronously and can trigger a sequence of other actions.

In a Type file, the @uiSdlActionCreator annotation defines a pure function that returns the JSON structure of the action that will take place. The @uiSdlReducer annotation defines a function that applies the action to the current state. For more information see Redux Reducers. The @uiSdlEpic annotation defines a function that, given the action executes asynchronous operations and ultimately triggers a sequence of new actions.

Filter panels and component data

Generally, SDL components have a @uiSdlActionCreator (e.g. updateDataFilterAction) and two @uiSdlEpic's (e.g. dataFilterUpdateEpic and filterSubmitEpic) that allow the component to be filtered by a filter panel. To ensure that your new component is compatible with a filter panel, declare these functions in the Type file and define them in the component's .ts implementation file.

Note: They are defined almost identically among SDL Components, so look at other Type files for reference (e.g. UiSdlTimeseriesLineBarChart.ts).

Component Implementation

Circular Dependencies

We recommend single-responsibility principle and do not allow circular import in component implementations. UiBundler will fail if circular dependencies are detected and the error message will show the list of all circular dependencies found.

  • Here is an example of circular dependencies:

    ExampleComponentA.ts

    TypeScript
    import { exampleFunctionB } from '@c3/ui/ExampleComponentB';

    ExampleComponentB.ts

    TypeScript
    import { exampleFunctionA } from '@c3/ui/ExampleComponentA';

    This will fail UiBundler with the following error message:

    Text
    Circular dependency detected in files:
    [ExampleComponentA.ts, ExampleComponentB.ts]

    In this case, you can create a new file ExampleComponentC and move exampleFunctionA related functionalities from ExampleComponentA to ExampleComponentC. And ExampleComponentB.ts would be the following after the change.

    ExampleComponentB.ts

    TypeScript
    import { exampleFunctionA } from '@c3/ui/ExampleComponentC';
  • Another kind of circular dependencies is importing defaultValues from itself. For example:

    ExampleSelfImportingComponent.ts

    TypeScript
    import { defaultValues } from '@c3/ui/ExampleSelfImportingComponent';

    This will also fail UiBundler with the following error message:

    Text
    Circular dependency detected in files:
    [ExampleSelfImportingComponent.ts]

    In this case, you should not import defaultValues but use it as a global variable.

    TypeScript
    /* global defaultValues */
Was this page helpful?