C3 AI Documentation Home

Reusable and non-reusable components

C3 AI Platform has a rich library of UI components, allowing you to incorporate data visualizations and build workflows into your application. You can also extend the library of available components, by building your components. This allows you to fully tailor visualizations and workflow to the needs of your end users.

Reusability as a decision factor

Determine how often you would reuse these features in that particular application or across the application. If you plan to not reuse them, create a custom component instance. If they are highly reusable, create a custom component Type.

Overview of custom component instances

A custom component instance implements one-off features by replacing any JSON component with a TSX file. This single-use component can be fully implemented and referenced as a TSX file instead of a JSON component.

Characteristics of custom components instances

  • They don't receive props. The component author must manage the data and the props by utilizing the shared state, sending requests, or hard-coding properties. The component renders similarly to how App components render in a default React application.
  • They can import other component instances, SDL components, sdl-react components, or any other external React components available.
  • They are overridable at the file level.

A Note about custom SCSS files for React-only components

Custom .scss files can be used to style React-only components. These .scss files are not backed by a type, and must live in the /src/ui/styling folder.

Importing .scss files

  • Currently, all .scss files will be available at the path @c3/ui/[YourCustomComponentStyles.scss]

Creating React-only components

Your React-only component will work like any other React component with a couple of special rules.

Placing your component

Custom React-only component instances must live in the ui/c3/src/customInstances folder, must use a component ID as the file name and have a .tsx file extension. As long as the component is in the correct folder with the correct extension, it will be picked up correctly.

Using external react libraries in your component

External libraries can be used in your custom components as long as the external libraries are installed in the correct runtime. To install a library into the runtime of your application, you can add it to js-webpack_c3.json in the {yourPackage}/metadata/ImplLanguage.Runtime as seen in the uiDemo file below:

JSON
{
  "meta": {
    "deserSource": "meta://uiDemo/metadata/ImplLanguage.Runtime/js-webpack_c3.json"
  },
  "name": "js-webpack_c3",
  "libraries": ["npm @material-ui/core@3.9.4"]
}

Using your component

There are two main ways your custom component instances can be consumed.

  • First, if your component does not require any props, it can be used directly in a .json file and consumed as a UiSdlComponentRef. For example, we can see the use of SDLDemo.CustomComponent, a React-only custom component instance, in this json:
JSON
{
  "type": "UiSdlLayoutNavMenu",
  "navMenu": {
    "id": "SDLDemo.NavMenu"
  },
  "children": [
    {
      "id": "SDLDemo.CustomComponent"
    }
  ]
}
  • Second, a custom component can also be used directly within another custom component. For example, we can see that SDLDemo.CustomComponent can also be imported by another custom component by importing it with the @c3/ui/components/{componentName} import.
TypeScript
import * as React from 'react';
import CustomComponent from '@c3/ui/components/SDLDemo.CustomComponent';

const PageContainer = () => {
  return <CustomComponent />;
};

export default PageContainer;

Overview of custom component types

Custom components can be implemented as C3 AI Types and used in JSON files. This means they resemble the components in the Component Library and can benefit from more granular overridability and introspection that drives C3 AI Platform tools.

Characteristics of custom component types

  • They receive props.
  • Authors need to define the props as a C3 AI Type.
  • They can import other component instances, SDL components, and external React components.
    • Note that if an external component library is required, such as Semantic UI or Material UI, this must be included in the runtime declaration for that C3 AI package.
  • The applications use them as JSON files.
  • They can be overridden through a JSON-patch.

You can use one of the following approaches to manage the data rendered in your newly created component.

  • Utilize the data management capability in the C3 AI UI Stack.
  • Manually manage the data of the component.

Both approaches import and use a custom SCSS file to style the newly created component.

Use custom SCSS for the newly created component

Use the following example to create a custom SCSS file for your newly created component.

Create a SCSS file named MyCustomGrid in the package/src/ui/styling folder.

SCSS
/**
 * The CSS variables are wrapped with the `var()`.
 * These variables respond to the theme and density changes.
 */

@mixin base-cell {
  border: 1px solid var(--c3-style-componentBorderColor);
  padding: var(--c3-style-smallSizePadding) var(--c3-style-mediumSizePadding);
  text-align: left;
  max-width: 33%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--c3-style-fontSizeBody);
}

td {
  @include base-cell;
  background-color: var(--c3-style-componentBackgroundColor);
  color: var(--c3-style-fontColor);
}

th {
  @include base-cell;
  background-color: var(--c3-style-zebraStripeBackgroundColor);
}

table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}

This example uses the var function to determine the values for various properties, such as font size and background color. If you use the application in different themes and densities for example light theme, dark theme, or classic or dense density, wrap your CSS variables with the var function.

Custom component with custom values in the themes will not update its styling in conjunction with out-of-the-box components if the theme or density setting is changed.

You can find a list of available CSS properties, also known as semantic tokens, by inspecting the UiSdlThemeTemplate and UiSdlDensityTemplate Types.

To learn more about theme and styles, go Theme and Style documentation.

Utilize the data management capability of C3 AI UI Stack for the newly created component

This example demonstrates creating a simple grid component to render a data table and use the data management capability of C3 AI UI Stack to manage data.

The following screenshot displays the custom components that are discussed in the next sections.

Image of final reusable component

To create this component, create the following five (5) files:

Note: The next few sections explicitly describe each file.

FileDescription
MyCustomGrid.c3typComponent Type file
MyCustomGridReact.c3typReact component Type file
MyCustomGridReact.tsxReact implementation
MyCustomGridDataSpec.c3typData specification Type
MyCustomGridDataSpec.tsData specification implementation

Create the component Type file - MyCustomGrid.c3typ

MyCustomGrid.c3typ defines the custom component.

Create a .c3typ file named MyCustomGrid.c3typ in the {package}/src/ui/components folder with the following configuration:

Type
/**
 * {package}/src/ui/components/MyCustomGrid.c3typ
 */
@typeScript
type MyCustomGrid extends UiSdlComponent<MyCustomGridDataSpec> {
  /**
   * The specification used for retrieving data displayed by the list.
   */
  @uiSdlDataSpec(dataDestinationField='data')
  dataSpec: ~
}

Adding Date and Decimal Customization

To create a component with the date and decimal customization:

  1. Create a CustomComponent.c3typ file. Add the field you want to format.

    Type
    type CustomComponent extends UiSdlComponent {
        textToDisplay: CustomFieldValue
    }
  2. Create a CustomFieldValue.c3typ file that contains a value to format.

    Type
    type CustomFieldValue {
        value: number
        format: UiSdlDynamicValueSpecParamFormat
    }
  3. Render the CustomComponent Type.

    Type
    {
        "type" : "CustomComponent"
        "textToDisplay" : {
            "value" : 50.123,
            "format" : {
            "type" : "UiSdlNumberParamKindFormat",
            "spec" : {
                "style" : "decimal",
                "maximumFractionDigits" : 2
                }
            }
        }
    }

Create React component Type - MyCustomGridReact.c3typ

This MyCustomGridReact.c3typ file gives you a React implementation for the grid. The component mixes or extends the ReactFunction and the previously created Type. The ReactFunction Type provides the abstract function which must be implemented in the MyCustomGridReact.tsx file.

Create a c3typ file named MyCustomGridReact.c3typ in the {package}/src/ui/components folder with the following configuration.

Type
/**
 * {package}/src/ui/components/MyCustomGridReact.c3typ
 */
type MyCustomGridReact extends ReactFunction mixes MyCustomGrid {
  render: ~ tsx-client
}

Implementation for the grid display - MyCustomGridReact.tsx

The React implementation to output the grid.

Create a tsx file named MyCustomGridReact.tsx in the {package}/src/ui/components folder with the following configuration.

TypeScript
/**
 * {package}/src/ui/components/MyCustomGridReact.tsx
 */
import React from 'react';
import { MyCustomGrid as MyCustomGridProps } from '@c3/types';

import '@c3/ui/MyCustomGrid.scss';

const MyCustomGrid = (props: MyCustomGridProps) => {
  const tableHeader = (
    <tr>
      {props.dataSpec.columnFields.map((columnField) => (
        <th>{columnField}</th>
      ))}
    </tr>
  );
  const tableRows = props.data?.objs?.map((datum) => (
    <tr key={datum.id}>
      {props.dataSpec.columnFields.map((columnField) => (
        <td>{datum[columnField]}</td>
      ))}
    </tr>
  ));

  return (
    <table>
      {tableHeader}
      {tableRows}
    </table>
  );
};

export default MyCustomGrid;

Use a JSON configuration file to add the component to the application in the {package}/ui/c3/meta/MyApp/MyApp.TranslationTable.json, as shown in the following example:

JSON
{
  "type": "UiSdlConnected<MyCustomGrid>",
  "component": {
    "dataSpec": {
      "dataType": "Translation",
      "columnFields": ["id", "key", "value"]
    }
  }
}

Create data specification Type - MyCustomGridDataSpec.c3typ

The type file specifies how data is fetched and returned to the component.

Create a c3typ file named MyCustomGridDataSpec.c3typ in the {package}/src/ui/components folder with the following configuration.

Type
/**
 * {package}/src/ui/components/MyCustomGridDataSpec.c3typ
 */
type MyCustomGridDataSpec mixes UiSdlFieldBasedDataSpec {
  toPartiallyAppliedActions: ~ ts-client

  /**
   * The names of the columns in the data
   */
  columnFields: [string]

  /**
   * The action used to retrieve data.
   */
  actionName: string = 'fetch'

  /**
   * The arguments to pass to the action, where the keys are the parameter
   * names and the values are the corresponding arguments.
   */
  actionArgs: json
}

Implement data specification - MyCustomGridDataSpec.ts

The data specification implementation takes the data spec and returns a partially applied action that determines the arguments for the data fetch.

Create a ts file named MyCustomGridDataSpec.ts in the {package}/src/ui/components folder with the following configuration.

TypeScript
/**
 * {package}/src/ui/components/MyCustomGridDataSpec.ts
 */
import { getDataTypeName } from '@c3/ui/UiSdlComponentDataSpec';

export function toPartiallyAppliedActions(
  dataSpec: MyCustomGridDataSpec,
  _config?: MyCustomGrid,
): UiSdlPartiallyAppliedActionInfo[] {
  const dataType = getDataTypeName(dataSpec);
  const actionName = 'fetch';
  const args = {
    spec: {
      limit: -1,
      include: dataSpec.columnFields.join(','),
      filter: '1 == 1',
    },
  };

  return [
    {
      partiallyAppliedAction: {
        typeName: dataType,
        actionName: actionName,
        args: args,
      },
    },
  ];
}

Manually manage the data fetch and render in the newly created component

In this approach, you do not need to define a dataSpec type and the related implementation file. Instead, use the built-in React hooks and the native JavaScript fetch function to fetch data and update the state of a component.

The following screenshot displays the custom components that are discussed in the next sections.

reusable-component-2

To create this component, create the following three (3) files:

The next few sections explicitly describes each file.

FileDescription
MyCustomGridCustomData.c3typComponent Type file
MyCustomGridCustomDataReact.c3typReact component Type file
MyCustomGridCustomDataReact.tsxReact implementation

The component type file - MyCustomGridCustomData.c3typ

MyCustomGridCustomData.c3typ defines the custom component.

Create a .c3typ file named MyCustomGridCustomData.c3typ in the {package}/src/ui/components folder with the following configuration:

Type
/**
 * {package}/src/ui/components/MyCustomGridCustomData.c3typ
 */

/*
 * Use the `UiSdlNoData` parameter
 * when you extend the UiSdlComponent,
 * to specify that there is
 * no C3 AI dataSpec associated with this Type.
 */
type MyCustomGridCustomData extends UiSdlComponent<UiSdlNoData>

Extend the ReactFunction type - MyCustomGridCustomDataReact.c3typ

MyCustomGridCustomDataReact.c3typ extends the abstract ReactFunction Type. The ReactFunction Type provides the abstract function which must be implemented in the React .tsx file.

Create a .c3typ file named MyCustomGridCustomData.c3typ in the {package}/src/ui/components folder with the following configuration:

Type
/**
 * {package}/src/ui/components/MyCustomGridCustomDataReact.c3typ
 */
type MyCustomGridCustomDataReact extends ReactFunction mixes MyCustomGridCustomData {
  render: ~ tsx-client
}

Use React to display data in the grid - MyCustomGridCustomDataReact.tsx

Create a tsx file named MyCustomGridCustomDataReact.tsx in the {package}/src/ui/components folder with the following configuration.

TypeScript
/**
 * {package}/src/ui/components/MyCustomGridCustomDataReact.tsx
 */
import React, { useState, useEffect } from 'react';

import '@c3/ui/MyCustomGrid.scss';

const MyCustomGridCustomData = () => {
  const [dataToRender, setDataToRender] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      // Publicly available dataset with no authentication required.
      const apiUrl = 'https://datausa.io/api/data?drilldowns=Nation&measures=Population';
      const response = await fetch(apiUrl);
      const data = await response.json();

      setDataToRender(data.data);
    };

    fetchData();
  }, []);

  const columnFields = ['Year', 'Nation', 'Population'];
  const tableHeader = (
    <tr>
      {columnFields.map((columnField) => (
        <th>{columnField}</th>
      ))}
    </tr>
  );

  return (
    <table>
      {tableHeader}
      {dataToRender.length &&
        dataToRender.map((datum) => (
          <tr key={datum.id}>
            {columnFields.map((columnField) => {
              const value = columnField === 'Population' ? datum[columnField].toLocaleString() : datum[columnField];
              return <td>{value}</td>;
            })}
          </tr>
        ))}
    </table>
  );
};

export default MyCustomGridCustomData;

Use a JSON configuration file to add your component to the application, as specified in the following example:

Type
/**
 * {package}/ui/c3/meta/MyApp/MyApp.PopulationTable.json
 */
{
  "type" : "UiSdlConnected<MyCustomGridCustomData>",
  "component" : {}
}

Custom Component interactions with other components and application state

Custom components are able to interact with other components through Redux and application state. For more information on these topics, see the document here.

In the sections below, we will demonstrate the following:

  1. How custom components can interact with other components
  2. How custom components can interact with application state

These will enable users to learn how to use custom components to interact with the other types of supported C3 AI UI Framework components. To see a list of components and when to use each one, see the section [Overview of external react components].

Interacting with other components

Using Redux we can read and or update another component's state. In this example, we will demonstrate how to use the Switch component from material-ui library to change whether or not the name column of a UiSdlDataGrid will be sorted as ascending or descending.

  1. We will start by creating a Switch component from material-ui library. To do so, we will create a component called SwitchComponent.tsxin {package}/ui/c3/src/mui directory.
TypeScript
import React from 'react';
import Switch from '@material-ui/core/Switch';
import FormControlLabel from '@material-ui/core/FormControlLabel';

const SwitchComponent = (props) => {
  return <FormControlLabel control={<Switch />} label={props.label} />;
};

export default SwitchComponent;
  1. Next, we will create a custom component instance called SDLDemo.SwitchContainer.tsx in {package}/ui/c3/src/customInstances that will reference SwitchComponent. We will do this because of two reasons: a. SDLDemo.SwitchContainer.tsx is a custom component instance that can be referenced as a UiSdlComponentRef. This means the component can be referenced from declarative components or imported from other custom components. In the following example, we will use this component and reference it from a declarative component. b. SwitchComponent is a custom component that can be imported and reused by other custom components, but is not a UiSdlComponentRef. This type of component is considered a external react component that can take in props from the component instance it is declared in. In this example, the SwitchComponent takes in the label prop that will set the label of the Switch.

For more details on the differences between custom component instances and external react components, please see the section [Overview of external React components].

TypeScript
import React from 'react';
import SwitchComponent from '@c3/app/ui/src/mui/SwitchComponent';

const SwitchContainer = () => {
  return (
    <div>
      <SwitchComponent label="Change the sort of the name field" />
    </div>
  );
};

export default SwitchContainer;
  1. Next, we will create a declarative component in {package}/ui/c3/meta/SDLDemo.DataGridSortColumn.json that uses UiSdlDataGrid to display some data from the SDLDemoMachine entity type.
JSON
{
  "type": "UiSdlConnected<UiSdlDataGrid>",
  "component": {
    "header": {
      "title": "Data Grid"
    },
    "paginationConfig": {
      "pageSize": 10,
      "pagination": true
    },
    "filterBar": true,
    "dataSpec": {
      "dataType": "SDLDemoMachine",
      "columnFields": [
        {
          "fieldName": "name",
          "label": "Name",
          "searchable": true,
          "sortable": true
        },
        {
          "fieldName": "statusLabel",
          "label": "SDLDemoMachine.status",
          "searchable": true
        }
      ]
    }
  }
}
  1. Now that we have created all of our desired components, we need to create a component to display both the SDLDemo.DataGridSortColumn and SDLDemo.SwitchContainer components. We will create a file in {package}/ui/c3/meta/SDLDemo.CustomComponentInteractionPage.json to display both components like so:
JSON
{
  "type": "UiSdlConnected<UiSdlLayoutMetadataNavMenu>",
  "component": {
    "navMenu": {
      "id": "SDLDemo.NavMenu"
    },
    "children": [
      {
        "id": "SDLDemo.SwitchContainer"
      },
      {
        "id": "SDLDemo.DataGridSortColumn"
      }
    ]
  }
}
  1. We need to change the SwitchComponent so that its checked value is set based on what the SDLDemo.DataGridSortColumn component's name column's sortColumn.descending value set to. In order to do so, you need to use the addReduxActionListener and getConfigFromState functions.

The addReduxActionListener function is useful for when you need to listen to a component action. It takes in two parameters:

  • The action to listen to.
  • A callback that contains two parameters - the action and current Redux state at that moment.

You can find the existing actions to listen to on the <Component>.c3typ with the @uiSdlActionCreator(actionType='<Action To Listen>') annotation.

  1. In the addReduxActionListener callback function, we can then listen to the SDLDemo.DataGridSortColumn component's INITIAL_RENDER action, and we can call getConfigFromState to retrieve the sortColumn.descending value from the SDLDemo.DataGridSortColumn component.

The getConfigFromState function will take in three parameters:

  • The ID of the component to retrieve the state from.
  • Reference to the redux state
  • An array of strings that specifies the path to retrieve the necessary metadata for your configuration. For more information on this, see the document here to understand how to retrieve values from redux.

The SwitchComponent can be updated to read the sortColumn.descending configuration from SDLDemo.DataGridSortColumn by using addReduxActionListener and getConfigFromState functions to initialize the Switch component's checked field.

TypeScript
import React from 'react';
import Switch from '@material-ui/core/Switch';
import { ReactReduxContext } from 'react-redux';
import { getConfigFromState } from '@c3/ui/UiSdlConnected';
import FormControlLabel from '@material-ui/core/FormControlLabel';

const SwitchComponent = (props) => {
  const [isChecked, setIsChecked] = React.useState(false);

  addReduxActionListener(`${DATA_GRID_COMPONENT_ID}.INITIAL_RENDER`, (action, state) => {
    const sortColumnDescending = getConfigFromState(DATA_GRID_COMPONENT_ID, state, ['sortColumn', 'descending']);
    setIsChecked(sortColumnDescending);
  });

  return <FormControlLabel control={<Switch checked={isChecked} />} label={props.label} />;
};

export default SwitchComponent;

With these changes, the Switch component's checked state is set based on the name sortColumn.descending value.

  1. The next thing we would like to do is to update the sortColumn.descending value when the user toggles the Switch component. In order to do so, we have to dispatch a redux action to trigger a reducer or epic to update the sortColumn.descending configuration of SDLDemo.DataGridSortColumn and change the sorting criteria for the grid data.

Redux state is immutable and can only be updated when an action is dispatched and a corresponding reducer function is called to update the Redux state. In this example, we will go one step further and actually create an epic instead of just a reducer because an epic will help us interact with our backend. We can send a data request to send a request to the database and return the data sorted for us by column name. In our custom component, we need to create both an action and a epic in {package}/src/UiSdlDataGrid.c3typ so that we can sort the data by the column field name.

In UiSdlDataGrid.c3typ, we will create the action and an epic:

Type
@uiSdlActionCreator(actionType='SORT_CHANGE')
changeSortAction: function(id: string,
                            field: string,
                            descending: boolean,
                            sortNullsLast: boolean): UiSdlSortChangeAction ts-client

@uiSdlEpic(actionType='SORT_CHANGE')
sortChangeEpic: private function(actionStream: UiSdlActionsObservable,
                                        stateStream: UiSdlStatesObservable): UiSdlActionsObservable ts-client

and define the action and reducer in {package}/src/UiSdlDataGrid.ts:

TypeScript
export function changeSortAction(
  id: string,
  field: string,
  descending: boolean,
  sortNullsLast: boolean,
): UiSdlSortChangeAction {
  return {
    type: id + '.SORT_CHANGE',
    payload: {
      componentId: id,
      field: field,
      descending: descending,
      sortNullsLast: sortNullsLast,
    },
  };
}

export const sortChangeEpic: Epic<AnyAction, AnyAction, ImmutableReduxState> = (actionStream, stateStream) => {
  return actionStream.pipe(
    mergeMap(function (action: UiSdlSortChangeAction) {
      const { componentId, descending, sortNullsLast } = action.payload;
      const dataSourceId = getCollectionDataSourceId('dataSpec', componentId);
      const descendingString = descending ? 'descending' : 'ascending';
      // This has to be true if descending and false if ascending for nulls to always be sorted last
      const fetchNullsLast = sortNullsLast ? descending : false;
      let orderString;
      if (action.payload.field) {
        orderString = descendingString + '(' + action.payload.field + ', ' + fetchNullsLast + ')';
      } else {
        orderString = '';
      }
      return of(
        mergeArgumentsAction(
          dataSourceId,
          {
            spec: {
              order: orderString,
            },
          },
          componentId,
        ),
        requestDataAction(dataSourceId),
        changeSortColumnAction(componentId, action.payload.field, action.payload.descending),
      );
    }),
  );
};
  1. Next we need to create a new file in {package}/src/UiSdlSortChangeAction.c3typ to listen to actions of type SORT_CHANGE
Type
type UiSdlSortChangeAction mixes UiSdlReduxAction<UiSdlSortChangePayload>

and create another new file in {package}/src/UiSdlSortChangePayload.c3typ which will specify how to change the descending for the name field:

Type
type UiSdlSortChangePayload mixes UiSdlComponentActionPayload {

  /**
   * The field to sort on.
   */
  field: string

  /**
   * True to sort in descending order, false to sort in descending order.
   */
  descending: boolean

  /**
   * If true, null values will come before non-null values. If false, null values will come after non-null values.
   */
  sortNullsLast: boolean
}
  1. With these changes, SwitchComponent we can dispatch the SORT_CHANGE action to change whether the sortColumn.descending value should be acending or descending based on the Switch component's checked field value.
TypeScript
import React from 'react';
import Switch from '@material-ui/core/Switch';
import { ReactReduxContext } from 'react-redux';
import { getConfigFromState } from '@c3/ui/UiSdlConnected';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import { useDispatch } from '@c3/ui/UiSdlUseDispatch';

const SwitchComponent = (props) => {
  const dispatch = useDispatch();
  const [isChecked, setIsChecked] = React.useState(false);

  addReduxActionListener(`${DATA_GRID_COMPONENT_ID}.INITIAL_RENDER`, (action, state) => {
    const sortColumnDescending = getConfigFromState(DATA_GRID_COMPONENT_ID, state, ['sortColumn', 'descending']);
    setIsChecked(sortColumnDescending);
  });

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const checked = event.target.checked;
    dispatch({
      type: `${DATA_GRID_COMPONENT_ID}.SORT_CHANGE`,
      payload: {
        componentId: DATA_GRID_COMPONENT_ID,
        field: 'name',
        descending: checked,
      },
    });
    setIsChecked(checked);
  };

  return <FormControlLabel control={<Switch checked={isChecked} onChange={handleChange} />} label={props.label} />;
};

export default SwitchComponent;

Toggling the Switch component should now change whether the name column should be sorted in an ascending or descending manner.

Interacting with application state

Application state is useful when you need to share state between multiple components. In the example below, we will create an TextField and a Button from the material-ui library and use the text input from the TextField to configure the header text of a UiSdlDataGrid.

  1. We will start by creating the Input and Button component. To do so, we will create a component called SDLDemo.CustomApplicationStateDemo.tsx in {package}/ui/c3/src/customInstances directory and render both the TextField and Button component.
TypeScript
import React from 'react';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';

const CustomApplicationStateDemo = (props) => {
  return (
    <Grid container spacing={1} alignItems="flex-end">
      <Grid item xs>
        <TextField id="input-with-button" label="Input" fullWidth />
      </Grid>
      <Grid item>
        <Button variant="contained" color="primary">
          Update
        </Button>
      </Grid>
    </Grid>
  );
};

export default CustomApplicationStateDemo;
  1. Next, we need to create our application state. In order to do so, you need to create two files, a .c3typ file and a .json file. The .c3typ file contains the information about what the application state stores and allows us to leverage the application state capabilities. The .json file is used to create an instance of the application state that will enable other components to reference your application state by internally creating a slice in the redux state which can be referenced by any component.

We will create a .c3typ file in {package}/src/SDLDemoApplicationStateDemo.c3typ and it will contain a variable called headerText. This field will be used to update the header text for the instance of the UiSdlDataGrid component we will create next.

Type
@typeScript
type SDLDemoApplicationStateDemo mixes UiSdlApplicationState {
  headerText: string = 'Default Content'
}

Next, we will create our .json file in {package}/ui/c3/meta/SDLDemo.ApplicationStateDemo.json. The file will contain the defaultvalue of headerText which will be set to Default Content.

JSON
{
  "type": "SDLDemoApplicationStateDemo",
  "headerText": "Default Content"
}
  1. Now we will create a new file in {package}/ui/c3/meta/SDLDemo.GridApplicationStateDemo.json which will be an instance of UiSdlDataGrid and reference the application state instance SDLDemoApplicationStateDemo we created in the previous step. The UiSdlDataGrid component's header.title field will be able to read the value from application state because the header.title field takes in a UiSdlDynamicValueSpec where you can dynamically specify how to set the value for header.title. For more details, please see UiSdlDynamicValueSpec.

In order to set the header.title to the value in our application state parameter, we need to specify the following fields:

  • type to specify that the value we are retrieving is dynamic.
  • dynamicValue to specify a translation to dynamically set the value to a specific key
  • vars to specify what the variable in dynamicValue should be assigned to.

In order to create the dynamicValue properly, we need to navigate to a translation file in {package}/metadata/Translation/en.csv and in the package, specify the key we want to dynamically set.

en.csv should look like this:

CSV
id,locale,key,value
en.SDLDemo.GridApplicationState.title,en,SDLDemo.GridApplicationState.title,{title}

Now we should be able to dynamically set the header.title.dynamicValue as SDLDemo.GridApplicationState.title and to set the vars to the headerText in our application state like so:

JSON
{
  "type": "UiSdlConnected<UiSdlDataGrid>",
  "component": {
    "paginationConfig": {
      "pageSize": 10,
      "pagination": true
    },
    "header": {
      "title": {
        "type": "UiSdlDynamicValueSpec",
        "dynamicValue": "SDLDemo.GridApplicationState.title",
        "vars": {
          "title": {
            "type": "UiSdlApplicationStateValueParam",
            "id": "SDLDemo.ApplicationStateDemo",
            "path": "headerText"
          }
        }
      }
    },
    "dataSpec": {
      "dataType": "SDLDemoMachine",
      "columnFields": [
        {
          "fieldName": "name",
          "label": "Name",
          "searchable": true
        }
      ]
    }
  }
}
  1. Next, we will create a file in {package}/ui/c3/meta/SDLDemo.CustomApplicationStateInteractionPage.json to display all the components together. We have added applicationStateRef field so that components in the SDLDemo.CustomApplicationStateInteractionPage.json file will have access to SDLDemo.ApplicationStateDemo we created in the previous step.
JSON
{
  "type": "UiSdlConnected<UiSdlLayoutMetadataNavMenu>",
  "applicationStateRef": {
    "type": "UiSdlApplicationStateRef",
    "id": "SDLDemo.ApplicationStateDemo"
  },
  "component": {
    "navMenu": {
      "id": "SDLDemo.NavMenu"
    },
    "children": [
      {
        "id": "SDLDemo.CustomApplicationStateDemo"
      },
      {
        "id": "SDLDemo.GridApplicationStateDemo"
      }
    ]
  }
}
  1. Once this is done, we can update SDLDemo.CustomApplicationStateDemo to read the headerText from application state. In order to do so, we can use the addReduxActionListener and the getConfigFromApplicationState functions.

For more information on addReduxActionListener, please see the section about interacting with other components.

The getConfigFromApplicationState function will take in three parameters:

  • The ID of the application to retrieve the state from.
  • A reference to the redux state
  • An array of strings that specifies the path to retrieve the necessary metadata for your configuration. For more information on this, see the document here to understand how to retrieve values from redux.

In the example below, we will initialize the text input of the TextField component to be initialized with the value from SDLDemo.ApplicationStateDemo.headerText.

TypeScript
import React from 'react';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import { ReactReduxContext } from 'react-redux';
import { getConfigFromApplicationState } from '@c3/ui/UiSdlApplicationState';

const CustomApplicationStateDemo = (props) => {
  const [text, setText] = useState('');
  const APPLICATION_STATE_ID = 'SDLDemo.ApplicationStateDemo';

  // Needs to be called after the application is first initialized so that the input can be set
  // to the value from application state.
  addReduxActionListener(`${APPLICATION_STATE_ID}.APPLICATION_STATE_INITIALIZE`, (action, state) => {
    const headerText = getConfigFromApplicationState(APPLICATION_STATE_ID, state, ['headerText']);
    setText(headerText);
  });

  return (
    <Grid container spacing={1} alignItems="flex-end">
      <Grid item xs>
        <TextField id="input-with-button" label="Input" value={text} fullWidth />
      </Grid>
      <Grid item>
        <Button variant="contained" color="primary">
          Add
        </Button>
      </Grid>
    </Grid>
  );
};

export default CustomApplicationStateDemo;
  1. Now that we can read from our application state, we also want to be able to update the headerText value in our application state. In order to do so, we want to enable the user to type in some text in the TextField component, and when the user clicks the Button, the headerText value from our application state will be updated. Because the SDLDemo.GridApplicationStateDemo component's header.title field listens for changes on the headerText field in application state, the header.title will also be updated to the submitted text value.

To do so, we need to update SDLDemoApplicationStateDemo to contain an action and a reducer so that we can update application state properly. Actions specify what change you want to make to your redux state and reducers specify how to update the redux state as needed.

Now we will update SDLDemoApplicationStateDemo.c3typ to contain the action and reducer that will modify the headerText value in application state:

Type
@typeScript
type SDLDemoApplicationStateDemo mixes UiSdlApplicationState {
  headerText: string = 'Default Content'

  /**
   * Triggered when we want to update the header text stored in application state
   */
  @uiSdlActionCreator(actionType='UPDATE_HEADER_TEXT')
  updateHeaderTextAction: private function(stateId: string, newHeaderText: string): UiSdlReduxAction ts-client

  /**
   * Listens for actions of type `UPDATE_HEADER_TEXT`, and updates the header text.
   *
   * @param state
   *           Redux state
   * @param action
   *           Redux action
   * @returns a new Redux state
   */
  @uiSdlReducer(actionType='UPDATE_HEADER_TEXT')
  headerTextUpdateReducer: private inline function(state: !UiSdlReduxState,
                                                    action: UiSdlReduxAction): UiSdlReduxState ts-client
}
  1. Then we can create a file in {package}/src/SDLDemoApplicationStateDemo.ts which will implement the functionality for updateHeaderTextAction and headerTextUpdateReducer:
TypeScript
import { UiSdlReduxAction } from '@c3/ui/UiSdlReduxAction';
import { ImmutableReduxState } from '@c3/ui/UiSdlConnected';
import { setConfigInApplicationState } from '@c3/ui/UiSdlApplicationState';

export function updateHeaderTextAction(stateId: string, newHeaderText: string): UiSdlReduxAction {
  return {
    type: stateId + '.UPDATE_HEADER_TEXT',
    payload: {
      stateId,
      headerText: newHeaderText,
    },
  };
}

export function headerTextUpdateReducer(state: ImmutableReduxState, action: UiSdlReduxAction): UiSdlReduxAction {
  return setConfigInApplicationState(action.payload.stateId, state, ['headerText'], action.payload.headerText);
}
  1. Once implemented, we can update SDLDemo.CustomApplicationStateDemo.tsx to then dispatch the UPDATE_HEADER_TEXT action so that when the Button is clicked, the input value from the TextField will be taken and will update the headerText in our application state. This will then update the header.title field on the SDLDemo.GridApplicationStateDemo component.
TypeScript
import React, { useState } from 'react';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import { getConfigFromApplicationState, setConfigInApplicationState } from '@c3/ui/UiSdlApplicationState';
import { addReduxActionListener } from '@c3/app/ui/src/epicRegistry';
import { useDispatch } from '@c3/ui/UiSdlUseDispatch';

const CustomApplicationStateDemo = (props) => {
  const dispatch = useDispatch();
  const [text, setText] = useState('');
  const APPLICATION_STATE_ID = 'SDLDemo.ApplicationStateDemo';

  // Needs to be called after the application is first initialized so that the input can be set to the value from
  // application state.
  addReduxActionListener(`${APPLICATION_STATE_ID}.APPLICATION_STATE_INITIALIZE`, (action, state) => {
    const headerText = getConfigFromApplicationState(APPLICATION_STATE_ID, state, ['headerText']);
    setText(headerText);
  });

  const handleInputChange = (event) => {
    setText(event.target.value);
  };

  const handleClick = () => {
    dispatch({
      type: `${APPLICATION_STATE_ID}.UPDATE_HEADER_TEXT`,
      payload: {
        stateId: APPLICATION_STATE_ID,
        headerText: text,
      },
    });
  };

  return (
    <Grid container spacing={1} alignItems="flex-end">
      <Grid item xs>
        <TextField id="input-with-button" label="Input" value={text} onChange={handleInputChange} fullWidth />
      </Grid>
      <Grid item>
        <Button variant="contained" color="primary" onClick={handleClick}>
          Update
        </Button>
      </Grid>
    </Grid>
  );
};

export default CustomApplicationStateDemo;

When a new input is provided in the TextField and the Button is clicked, SDLDemo.GridApplicationStateDemo component's header.title field should be updated correctly based on the update in application state.

Retrieving and using data for Custom Components

When creating your own custom components, you'll often need to fetch and display data from specific data sources. In the following example, we will create a custom component using the material-ui library Table component to display data from the SDLDemoMachine entity type. The SDLDemoMachine data will be stored in application state so it can be used in your custom component.

By fetching and storing SDLDemoMachine data in the application state, both this UI component, and any other UI component can get information and subscribe to changes to the data, making the logic easier to manage, and minimizing the amount of requests to the server.

  1. First, we should create a custom component called SDLDemo.DemoFetchGrid.tsx in {package}/ui/c3/customInstances/SDLDemo.DemoFetchGrid.tsx and import the material-ui library to display the Table component.
TypeScript
import React from 'react';
import Table from '@mui/material-ui/core/Table';
import TableHead from '@mui/material-ui/core/TableHead';
import TableRow from '@mui/material-ui/core/TableRow';
import TableCell from '@mui/material-ui/core/TableCell';
import TableBody from '@mui/material-ui/core/TableBody';

const DemoFetchGrid = () => {
  const rows = [{ name: 'PF-193', category: 'I', status: 'RUNNING' }];

  return (
    <Table>
      <TableHead>
        <TableRow>
          <TableCell>Name</TableCell>
          <TableCell>Category</TableCell>
          <TableCell>Status</TableCell>
        </TableRow>
      </TableHead>
      <TableBody>
        {rows.map((row) => {
          return (
            <TableRow key={row.name}>
              <TableCell>{row.name}</TableCell>
              <TableCell>{row.category}</TableCell>
              <TableCell>{row.status}</TableCell>
            </TableRow>
          );
        })}
      </TableBody>
    </Table>
  );
};

export default DemoFetchGrid;
  1. Next, we should create a function to query your data. In this example, we will create an Entity Type in {package}/src/SDLDemoMachine.c3typwith some fields.
Type
entity type SDLDemoMachine mixes SeedData, Named {
    category: string
    status: string enum('RUNNING', 'NOT_RUNNING')
    getMachinesByStatus: function(name: string): [SDLDemoMachine] js-server
}

We will also create the implementation logic to query the desired data. Create a file in {package}/src/SDLDemoMachine.js with the following implementation:

JavaScript
function getMachinesByStatus(category) {
  return SDLDemoMachine.fetch(Filter.eq('status', status));
}

The example above uses the Filter to query for machines with a particular status.

  1. Now that we have a data model that allows persisting data, we should make sure we have seed data so we can test the application logic. Create a new file in {package}/seed/SDLDemoMachine/SDLDemoMachine.csv and add the following data about machines:
CSV
name,category,status
PF-193,I,RUNNING
LA-655,II,NOT_RUNNING

This is the data that we will be displaying on the grid.

  1. Now that we've created the data and the SDLDemoMachine type, we can create an instance of application state. In our application state instance, we will create the FETCH_MACHINES_BY_STATUS action on SDLDemoApplicationStateDemo so we can to query the SDLDemoMachine entity.

For more details about creating an instance of application state, please see the section Interacting with application state.

We will create the .c3typ file in {package}/src/SDLDemoApplicationStateDemo.c3typ:

Type
@typeScript
type SDLDemoApplicationStateDemo mixes UiSdlApplicationState

In this .c3typ file, we will add three functions - fetchMachinesByStatusAction to create the redux action for fetching the machine by status, fetchMachinesByStatus to query for the SDLDemoMachine data, and storeMachinesReducer to store the SDLDemoMachine data in Redux application state.

Type
@typeScript
type SDLDemoApplicationStateDemo mixes UiSdlApplicationState {

  @uiSdlActionCreator(actionType='FETCH_MACHINES_BY_STATUS')
  fetchMachinesByStatusAction: private final function(applicationStateId: string):
  !UiSdlReduxAction<UiSdlComponentActionPayload> ts-client

  @uiSdlEpic(actionType='FETCH_MACHINES_BY_STATUS')
  fetchMachinesByStatus: private function(actionStream: UiSdlActionsObservable,
                                    stateStream: UiSdlStatesObservable): UiSdlActionsObservable ts-client

  @uiSdlReducer(actionType='STORE_MACHINES')
  storeMachinesReducer: private member function(actionStream: !UiSdlActionsObservable,
                                           stateStream: !UiSdlStatesObservable): !UiSdlActionsObservable ts-client
}
  1. Then we need to create the implementation of the functions. In {package}/src/SDLDemoMachine.ts, we will create the following functions:
TypeScript
export function fetchMachinesByStatusAction(componentId: string) {
  return {
    type: componentId + '.FETCH_MACHINES_BY_STATUS',
    payload: {
      applicationStateId: componentId,
    },
  };
}

export function fetchMachinesByStatus(
  actionStream: UiSdlActionsObservable,
  _stateStream: UiSdlStatesObservable,
): UiSdlActionsObservable {
  return actionStream.pipe(
    mergeMap(function (action: UiSdlLoadedDataHandleAction) {
      const { componentId } = action.payload;
      return ajax('SDLDemoMachine', 'getMachinesByStatus', { status: 'RUNNING' }).pipe(
        mergeMap(function (event) {
          const data = event.response;
          return of({
            type: `${componentId}.STORE_MACHINES`,
            payload: {
              componentId,
              data,
            },
          });
        }),
      );
    }),
  );
}

export function storeMachinesReducer(
  state: ImmutableReduxState,
  action: UiSdlLoadedDataHandleAction,
): ImmutableReduxState {
  const applicationStateId = action.payload.componentId;
  return setConfigInApplicationState(applicationStateId, state, 'machines', action.payload.data);
}

fetchMachinesByStatusAction specifies the Redux action to dispatch when retrieving the SDLDemoMachine data.

fetchMachinesByStatus will use the ajax method to query data from a Type. In this case, we are querying the SDLDemoMachine Type and the getMachinesByStatus action. After the data is fetched, we will dispatch the action SDLDemo.ApplicationStateDemo.STORE_MACHINES which will be used to store the data in Redux.

storeMachinesReducer will store the data in metadata > applications > byId > SDLDemo.ApplicationStateDemo > machines Redux slice using the setConfigInApplicationState method.

  1. We then need to create a corresponding .json file in {package}/ui/c3/meta/SDLDemo.ApplicationStateDemo.json which is the instance of our application state. In this file we will create an effectTrigger to specify that when the application state is initialized, we will dispatch the SDLDemo.ApplicationStateDemo.FETCH_MACHINES_BY_STATUS action.
JSON
{
  "type": "SDLDemoApplicationStateDemo",
  "effectTriggers": [
    {
      "trigger": "SDLDemo.ApplicationStateDemo.APPLICATION_STATE_INITIALIZE",
      "actions": [
        {
          "type": "SDLDemo.ApplicationStateDemo.FETCH_MACHINES_BY_STATUS"
        }
      ]
    }
  ]
}

When SDLDemo.ApplicationStateDemo is initialized, it will dispatch the action SDLDemo.ApplicationStateDemo.APPLICATION_STATE_INITIALIZE which will in turn dispatch the SDLDemo.ApplicationStateDemo.FETCH_MACHINES_BY_STATUS action. Internally, the fetchMachinesByStatus epic is listening to the SDLDemo.ApplicationStateDemo.FETCH_MACHINES_BY_STATUS action; this epic will execute the server request that fetches the data. That epic then dispatches the action SDLDemo.ApplicationStateDemo.STORE_MACHINES, along with the fetched data as a payload. That action will trigger a reducer to store the result of the query in the metadata > applications > byId > SDLDemo.ApplicationStateDemo > machines Redux slice.

  1. Now that we have our custom component and our application state instance created, we need to put them both on the same page so that they can interact with each other. To do so, we will create a file in {package}/ui/c3/meta/SDLDemo.CustomComponentFetchPage.json and put both the CustomComponentFetch and SDLDemo.ApplicationStateDemo components together:
JSON
{
  "type": "UiSdlConnected<UiSdlLayoutMetadataNavMenu>",
  "applicationStateRef": {
    "type": "UiSdlApplicationStateRef",
    "id": "SDLDemo.ApplicationStateDemo"
  },
  "component": {
    "navMenu": {
      "id": "SDLDemo.NavMenu"
    },
    "children": [
      {
        "id": "SDLDemo.DemoFetchGrid"
      }
    ]
  }
}

And an associated route to view the page in {package}/metadata/UiSdlRoute/UiRoute.csv:

CSV
targetModuleName,targetPageName,name,urlPath
SDLDemo,CustomComponentFetchPage,/sdl-demo/custom/custom-component-fetch-data,/sdl-demo/custom/custom-component-fetch-data
  1. Now in {package}/ui/c3/src/SDLDemo.DemoFetchGrid.tsx, we need to listen to the SDLDemo.ApplicationStateDemo.FETCH_MACHINES_BY_STATUS action. This is because SDLDemo.ApplicationStateDemo.FETCH_DATA internally calls the SDLDemo.ApplicationStateDemo.STORE_MACHINES action to store the data in the metadata > applications > byId > SDLDemo.ApplicationStateDemo > machines Redux slice. In order to retrieve this data, we need extend our custom component to add a function called dataStoreListener that will use the addReduxActionListener function to listen when the SDLDemo.ApplicationStateDemo.STORE_MACHINES action is called. When this function is called, the getConfigFromApplicationState function can be used to retrieve the data in the SDLDemo.ApplicationStateDemo > machines Redux slice and to display it in our custom component.

We can update {package}/ui/c3/src/SDLDemo.DemoFetchGrid.tsx like so:

TypeScript
import React from 'react';
import Table from '@mui/material-ui/core/Table';
import TableHead from '@mui/material-ui/core/TableHead';
import TableRow from '@mui/material-ui/core/TableRow';
import TableCell from '@mui/material-ui/core/TableCell';
import TableBody from '@mui/material-ui/core/TableBody';
import { addReduxActionListener } from '@c3/app/ui/src/epicRegistry';
import { getConfigFromApplicationState } from '@c3/ui/UiSdlApplicationState';

const DemoFetchGrid = () => {
  const [rows, setRows] = React.useState([]);

  const APPLICATION_STATE_ID = 'SDLDemo.ApplicationStateDemo';

  const dataStoreListener = () => {
    addReduxActionListener(`${APPLICATION_STATE_ID}.STORE_MACHINES`, (action, state) => {
      const machines = getConfigFromApplicationState(APPLICATION_STATE_ID, state, ['machines'])?.toJS();
      setRows(machines);
    });
  };

  React.useEffect(() => {
    dataStoreListener();
  }, []);

  return (
    <Table>
      <TableHead>
        <TableRow>
          <TableCell>Name</TableCell>
          <TableCell>Category</TableCell>
          <TableCell>Status</TableCell>
        </TableRow>
      </TableHead>
      <TableBody>
        {rows.map((row) => {
          return (
            <TableRow key={row.name}>
              <TableCell>{row.name}</TableCell>
              <TableCell>{row.category}</TableCell>
              <TableCell>{row.status}</TableCell>
            </TableRow>
          );
        })}
      </TableBody>
    </Table>
  );
};

export default DemoFetchGrid;

With the example above, the custom component is just displaying the SDLDemoMachine data from application state, but it can now modify and enhance the data as needed.

Retrieving data within Custom Components

In other use cases, you may want the component itself to fetch and handle the data. This example will walk through how to make a custom component that fetches data directly.

  1. First, we should create a custom component called CustomFetchComponent.tsx in {package}/ui/c3/src/CustomFetchComponent.tsx and import the material-ui library to display the Table component.
TypeScript
import React from 'react';
import Table from '@mui/material-ui/core/Table';
import TableHead from '@mui/material-ui/core/TableHead';
import TableRow from '@mui/material-ui/core/TableRow';
import TableCell from '@mui/material-ui/core/TableCell';
import TableBody from '@mui/material-ui/core/TableBody';

const CustomFetchComponent = (props) => {
  const rows = [{ name: 'PF-193', category: 'I', status: 'RUNNING' }];

  return (
    <Table>
      <TableHead>
        <TableRow>
          <TableCell>Name</TableCell>
          <TableCell>Category</TableCell>
          <TableCell>Status</TableCell>
        </TableRow>
      </TableHead>
      <TableBody>
        {rows.map((row) => {
          return (
            <TableRow key={row.name}>
              <TableCell>{row.name}</TableCell>
              <TableCell>{row.category}</TableCell>
              <TableCell>{row.status}</TableCell>
            </TableRow>
          );
        })}
      </TableBody>
    </Table>
  );
};

export default CustomFetchComponent;
  1. Next, we should create a function to query your data. In this example, we will create an Entity Type in {package}/src/SDLDemoMachine.c3typwith some fields.
Type
entity type SDLDemoMachine mixes SeedData, Named {
    category: string
    status: string enum('RUNNING', 'NOT_RUNNING')
    getMachinesByStatus: function(name: string): [SDLDemoMachine] js-server
}

We will also create the implementation logic to query the desired data. Create a file in {package}/src/SDLDemoMachine.js with the following implementation:

JavaScript
function getMachinesByStatus(category) {
  return SDLDemoMachine.fetch(Filter.eq('status', status));
}

The example above uses the Filter to query for machines with a particular status.

  1. Now that we have a data model that allows persisting data, we should make sure we have seed data so we can test the application logic. Create a new file in {package}/seed/SDLDemoMachine/SDLDemoMachine.csv and add the following data about machines:
CSV
name,category,status
PF-193,I,RUNNING
LA-655,II,NOT_RUNNING

This is the data that we will be displaying on the grid.

  1. Now that we have our custom component, we can put them both on the same page to also have the UiSdlNavMenu and any other components for the page. To do so, we will create a file in {package}/ui/c3/meta/SDLDemo.CustomComponentFetchPage.json and put both the CustomComponentFetch component there:
JSON
{
  "type": "UiSdlConnected<UiSdlLayoutMetadataNavMenu>",
  "component": {
    "navMenu": {
      "id": "SDLDemo.NavMenu"
    },
    "children": [
      {
        "id": "CustomFetchComponent"
      }
    ]
  }
}

And an associated route to view the page in {package}/metadata/UiSdlRoute/UiRoute.csv:

CSV
targetModuleName,targetPageName,name,urlPath
SDLDemo,CustomComponentFetchPage,/sdl-demo/custom/custom-component-fetch-data,/sdl-demo/custom/custom-component-fetch-data
  1. Now in {package}/ui/c3/src/CustomFetchComponent.tsx, we can add the UiSdlUseC3Action hook to fetch the data. Since we are trying to call getMachinesByStatus on SDLDemoMachine, we will set those as the actionName and typeName respectively. Additionally, if we want this fetch to happen more than once, we can optionally set the invalidators array to make the fetch happen more than once. In this case, we will use a button to re-fetch data.

We can update {package}/ui/c3/src/CustomFetchComponent.tsx like so:

TypeScript
import React from 'react';
import Table from '@mui/material-ui/core/Table';
import TableHead from '@mui/material-ui/core/TableHead';
import TableRow from '@mui/material-ui/core/TableRow';
import TableCell from '@mui/material-ui/core/TableCell';
import TableBody from '@mui/material-ui/core/TableBody';
import { useC3Action } from '@c3/ui/UiSdlUseC3Action';

const CustomFetchComponent = (props) => {
  const [rows, setRows] = React.useState([]);
  const [value, setValue] = React.useState(1);

  const APPLICATION_STATE_ID = 'SDLDemo.ApplicationStateDemo';

  const actionSpec = {
    typeName: 'SDLDemoMachine',
    actionName: 'getMachinesByStatus',
    argsArray: ['RUNNING'], // Pass status as positional argument
    invalidators: [value],
    method: 'POST',
  };

  const actionResponse = useC3Action(actionSpec);

  React.useEffect(() => {
    if (actionResponse.status === 'done') {
      setRows(actionResponse.data);
    }
  }, [actionResponse]);

  const updateFetch = () => {
    setValue(value + 1);
  };

  return (
    <>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>Name</TableCell>
            <TableCell>Category</TableCell>
            <TableCell>Status</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {rows.map((row) => {
            return (
              <TableRow key={row.name}>
                <TableCell>{row.name}</TableCell>
                <TableCell>{row.category}</TableCell>
                <TableCell>{row.status}</TableCell>
              </TableRow>
            );
          })}
        </TableBody>
      </Table>
      <button onClick={updateFetch}> Click here to re-fetch data </button>
    </>
  );
};

export default CustomFetchComponent;

With the example above, the custom component is just displaying the SDLDemoMachine data from server, but it can now modify and enhance the data as needed.

Overview of external React components

You can use external React components as building blocks for both custom component instances and custom component types.

You can author external React components outside the C3 AI UI Infrastructure, co-locate them with your C3 AI package ui code or just reuse a component from an existing component library such as Semantic UI or Material UI.

An example of such an implementation is the sdl-react component library that contains React presentational and atomic components that are reused to implement our SDL Component Library.

Characteristics of external React components

  • They are pure React components.
  • They don't define props as C3 AI Types, instead you can define them in the same file.
  • They are created inside an application package under ui/src/ and imported from @c3/app/ui/src. This makes them overridable at the file level.
  • Alternatively, they can be published as a third-party React library and imported from their NPM package, like sdl-react.
  • Can be imported from custom component instances or custom component types.

Differences between external React components and custom component instances

  1. Custom component instances are utilized as a UiSdlComponentRef. This allows them to be referenced in declarative components and imported in other custom component instances. They are located in the {package}/ui/c3/src/customInstances directory.
  2. Custom component instances do not accept any props. This is because they can be referenced through declarative components as a UiSdlComponentRef, which does not allow props to be passed in.
  3. External react components can be imported from other custom components, but should not be used as a UiSdlComponentRef. They are located anywhere in the {package}/ui/c3/src/** directory, except for the {package}/ui/c3/src/customInstances directory.
  4. External react components can accept props because they are only referenced through .tsx files located in {package}/ui/c3/src/**.
  5. External react components are not limited to .tsx files; they can also be .ts files. These files can contain helper functions or code that is reusable across any .ts or .tsx file in the {package}/ui/c3/src/** directory.

For an example illustrating the differences between custom component instances and external react components, please refer to the section [Interacting with other components].

Overview of component

Authoring MechanismFlexibilityReusabilityOwnershipIntrospectionOverridabilityDevelopment CostTechnical debt
Custom Component InstanceHighNoneAppNoneFile basisLowHigh*
External React ComponentHighMedium*AppNoneNone - File basisLowMedium*
Custom Component TypeHighMedium*AppMedium*JSON PatchMediumLow
SDL Component TypeLowHighStudioHighJSON PatchHighNone

*: Scores marked with an asterisk depend on the complexity of the implementation, and each application author can decide how thick or thin their abstractions are. The base score given takes into account the base balance between development cost and technical debt (maintenance cost).

  • Flexibility
    • Low means authors have to follow particular patterns and guidelines.
    • High allows authors to decide everything.
  • Reusability
    • None means they cannot be reused and require copy-paste to replicate the functionality.
    • Medium means they can be reused but props don't follow a particular pattern that can accelerate usage.
    • High means they follow a pattern that can accelerate usage.
  • Ownership
    • App means the application author will own them.
    • Studio means they get support and maintenance from the Studio team.
  • Introspection
    • None means the application cannot examine the props/data at runtime.
    • Medium means the application can gather data from the prop definitions as types.
    • High means the application follow a particular pattern and use annotations that provide more metadata.
  • Overridability
    • None means dependent packages cannot override.
    • File basis means dependent packages can override a whole file.
    • JSON Patch means dependent packages can override, extend or remove configurations in JSON files.
  • Development Cost
    • Low means it is straightforward to fully implement since it is pure React.
    • Medium means there are extra requirements for authoring.
    • High means authors need to consider a lot of abstractions, guidelines, and a particular definition of done.
  • Technical Debt
    • None means it conforms to SDL and follows implementation guidelines.
    • Low means it will need major refactoring and is compatible with most of the UI Infrastructure stack features.
    • Medium means it might include some patterns that are not documented, are application specific or don't conform with SDL and guidelines.
    • High means it is a one-off solution and reusability is done through copy-paste and/or will require refactoring to conform to SDL, guidelines, and UI Infrastructure stack features.

A Note about Custom SCSS Files

There are some conventions around bundling and deploying custom .scss files, as outlined below.

  • When a custom .scss file is backed by a custom React type, and the name of the .scss file matches that of the custom React type
    • The .scss file must live somewhere in the src folder.
  • When the .scss file is not backed by a type
    • The .scss file must live in the /src/ui/styling folder.

Known discrepancy in Development Mode bundling through VS Code

When running the UI bundler from VS Code, you might notice that any .scss file in the /src folder will be picked up and loaded by the repo watcher, however it is recommended to keep custom .scss files not backed by a type in the /src/ui/styling folder to avoid discrepancies between Production and Development mode.

Importing .scss files

  • Currently, all .scss files will be available at the path @c3/ui/[YourCustomComponentStyles.scss]

Example scenarios

The following section includes real life examples and use cases. The examples provided are found in the uiDemo package.

Whole page is a custom feature

If this custom feature doesn't need any overridability or reusability, the whole page should be implemented in a custom component instance.

  • Start UI Demo application and navigate to https://UI_DEMO_URL/custom/page.
  • Connect your VS Code to that application to inspect the source code. For this example, refer to:
Text
uiDemo
├── uiDemo.c3pkg.json
├── metadata
│   └── UiSdlRoute
│       └── UiRoute.csv
└── ui
    └── c3
        └── src
            └── customInstances
                └── SDLDemo.CustomPage.tsx

URL: /sdl-demo/custom/page

Implementation

SDLDemo.CustomPage.tsx - custom component instance for the whole page, it imports the navigation component instance and renders a Radar Chart

One component in one page is custom

  • Start UI Demo application and navigate to https://UI_DEMO_URL/custom/component.
  • Connect your VS Code to that application to inspect the source code. For this example, refer to:
Text
uiDemo
├── uiDemo.c3pkg.json
├── metadata
│   └── UiSdlRoute
│       └── UiRoute.csv
└── ui
    └── c3
        └── src
            └── customInstances
                └── SDLDemo.CustomComponentPage.json
                └── SDLDemo.CustomComponent.tsx

URL: /sdl-demo/custom/component
Implementation:
SDLDemo.CustomComponentPage.json - Component instance for the page
SDLDemo.CustomComponent.tsx - custom component instance to render a Radar Chart

The custom component has to be rendered multiple times with different properties

Connect your VS Code to that application to inspect the source code. For the following examples, refer to:

Text
uiDemo
├── uiDemo.c3pkg.json
├── metadata
│   |── UiSdlRoute
│   |    └── UiRoute.csv
|   |
|   └── ImplLanguage.Runtime
|        └── js-webpack_c3.json
├── src
│   |── ui
│       |── custom
│       |   └── UiCustomChartReact.c3typ
|       |
|       └── styling
|           └── ChipsArray.scss
└── ui
    └── c3
        │── src
        │    └── customInstances
        │        └── SDLDemo.CustomExternalComponent_One.tsx
        │        └── SDLDemo.CustomExternalComponent_Two.tsx
        │    └── mui
        |        └── ChipsArray.tsx
        └── meta
            └── Custom
                └── customType
                    └── SDLDemo.CustomComponentTypePage.json
                    └── SDLDemo.CustomComponentType_One.json
                    └── SDLDemo.CustomComponentType_Two.json

Two solutions:

1) Custom component instances with External React Component

URL: /sdl-demo/custom/external/
Implementation:
ChipsArray.tsx - Pure React component which imports material-ui
SDLDemo.CustomExternalComponent_One.tsx - Wrapper to pass props to ChipsArray.tsx
SDLDemo.CustomExternalComponent_Two.tsx - Wrapper to pass props to ChipsArray.tsx

  • This example highlights a pure React component, ChipsArray.tsx, which imports two external components from the material-ui library: Chip and Button.

    In order to use this third-party library, it needs to be added to the js-webpack_c3.json file:

    JSON
    {
      "meta": {
        "deserSource": "meta://uiDemo/metadata/ImplLanguage.Runtime/js-webpack_c3.json"
      },
      "name": "js-webpack_c3",
      "libraries": ["npm @material-ui/core@3.9.4"]
    }
  • To pass different props, a .tsx wrapper is created which passes different props to the pure React component. These wrappers are then referenced in the .json configuration file. While these props are not dynamic (cannot be changed at runtime), they do allow some level of flexibility.

  • Custom .scss may be imported at @c3/ui/[YourCustomScssFile.scss]

    JavaScript
    // ChipsArray.tsx
    
    import '@c3/ui/ChipsArray.scss';

    The custom .scss files should be created in src/ui/stying. See here for more information about custom styling.

  • The material-ui library must be added in the runtime for the C3 AI package uiDemo. See {@type ImplLanguage.Runtime} for more information.

    JSON
    // /uiDemo/metadata/ImplLanguage.Runtime/js-webpack_c3.json
    
    {
      "meta": {
        "deserSource": "meta://uiDemo/metadata/ImplLanguage.Runtime/js-webpack_c3.json"
      },
      "name": "js-webpack_c3",
      "libraries": ["npm @material-ui/core@3.9.4"]
    }

2) Custom component type with external react component UI Demo example

URL: /sdl-demo/custom/type/
Implementation:
SDLDemo.CustomComponentTypePage.json
SDLDemo.CustomComponentType_One.json
SDLDemo.CustomComponentType_Two.json
UiCustomChartReact.c3typ

Was this page helpful?