C3 AI Documentation Home

Follow the conventions for field-based components

Field-based components display structured records — objects with a consistent shape and a known set of fields. The UI Framework simplifies the process of creating reusable components and lets you expose them as standard React components or through Type APIs. Application developers configure these components declaratively using JSON.

This example builds a table component that fetches and displays WindTurbine records. The JSON configuration specifies which fields to fetch and how to render the data.

The following files define the example custom component, describe how to fetch data, implement the fetch logic, render the UI, and declare a JSON configuration for use in the application:

File PathDescription
{package}/src/ui/components/MyCustomGrid.c3typDefines the component
{package}/src/ui/components/MyCustomGridDataSpec.c3typDescribes how to fetch records
{package}/src/ui/components/MyCustomGridDataSpec.tsImplements the fetch logic for records
{package}/src/ui/components/WindTurbineGrid.tsxRenders the fetched records as a table
{package}/ui/c3/meta/WindTurbine.TranslationGrid.jsonDeclares a JSON instance of the component

Use structured data

Structured data enables the platform to power features like sorting, filtering, and pagination. Each record must use a consistent schema so the UI can display fields predictably. The following JSON shows a well-structured WindTurbine record:

JSON
{
  "id": "windturbine_42",
  "name": "Turbine 42",
  "location": "Texas",
  "status": "RUNNING"
}

Avoid using loosely structured records. The following example shows a record that cannot be displayed in a table or grid because its fields vary and don't map to a consistent schema:

JSON
{
  "id": "abc123",
  "metadata": {
    "values": ["offline", 70],
    "notes": "no location recorded"
  }
}

This kind of data doesn’t work well in grids or tables because it lacks predictable fields. The platform cannot infer how to sort, filter, or display these records.

When you use field-based components, bind them to structured data through a UiSdlFieldBasedDataSpec. This tells the platform how to fetch and shape the data so your component can render it cleanly—without writing custom data logic.

Step 1: Create a component Type with a data spec

To enable data fetching in your custom component, define a Type that extends UiSdlComponent. Add the @uiSdlDataSpec annotation to link the data fetch logic to a specific configuration field. This setup lets the platform handle data injection into your component automatically.

Type
// {package}/src/ui/components/MyCustomGrid.c3typ

@typeScript
type MyCustomGrid extends UiSdlComponent<MyCustomGridDataSpec> {
  /**
   * Configuration for the data shown in the grid.
   */
  @uiSdlDataSpec(dataDestinationField='data')
  dataSpec: ~
}

This declaration connects the platform-managed data fetch to the dataSpec field and tells the platform to store the results in props.config.component.data.

Step 2: Define a field-based data spec

Create a Type that mixes in UiSdlFieldBasedDataSpec. This signals to the platform that your component expects structured records and enables built-in support for columns, pagination, filtering, and sorting.

Type
// {package}/src/ui/components/MyCustomGridDataSpec.c3typ

@typeScript
type MyCustomGridDataSpec mixes UiSdlFieldBasedDataSpec {
  toPartiallyAppliedActions: ~ ts-client

  columnFields: [string]
  actionName: string = 'fetch'
  actionArgs: json
}

This Type provides metadata about what fields to load, what backend action to call, and what arguments to pass to that action.

Step 3: Implement the data fetch logic

Use UiSdlComponentDataSpec#toPartiallyAppliedActions to define how the platform should call the backend to fetch records. This function returns an array of action specifications that instruct the platform what data to retrieve and how.

TypeScript
// {package}/src/ui/components/MyCustomGridDataSpec.ts

// Import utility to extract the entity Type name (e.g., 'WindTurbine')
import { getDataTypeName } from '@c3/ui/UiSdlComponentDataSpec';
// Import the return type for the fetch config
import type { UiSdlPartiallyAppliedActionInfo } from '@c3/types';

export function toPartiallyAppliedActions(
  dataSpec: MyCustomGridDataSpec,       // The dataSpec config from your component JSON
  _config?: MyCustomGrid                // The component config (not used in this example)
): UiSdlPartiallyAppliedActionInfo[] {
  // Get the entity Type (e.g., 'WindTurbine') from the dataSpec
  const dataType = getDataTypeName(dataSpec);

  // Define the action to call on the backend Type
  const actionName = 'fetch';

  // Define the arguments to pass to the action
  const args = {
    spec: {
      limit: -1,                                     // Fetch all records (no limit)
      include: dataSpec.columnFields.join(','),     // Only fetch selected fields
      filter: '1 == 1'                               // Dummy filter that returns all rows
    }
  };

  // Return the fetch configuration as an array of actions
  return [
    {
      partiallyAppliedAction: {
        typeName: dataType,                          // Backend entity Type to query
        actionName: actionName,                      // Action to call ('fetch')
        args: args                                   // Arguments to pass to that action
      }
    }
  ];
}

The platform calls this function during rendering and uses the returned instructions to load data for your component.

Step 4: Render the data

The platform injects the fetched records into props.config.component.data. Use that field in your React component to render the results. In this example, a simple table layout shows the name, location, and status of each turbine.

Let’s say your fetch returns a list of WindTurbine records. The runtime injects the data like this:

TypeScript
{
  config: {
    component: {
      data: [
        {
          id: "windturbine_01",
          name: "Turbine 01",
          location: "Texas",
          status: "RUNNING"
        },
        {
          id: "windturbine_02",
          name: "Turbine 02",
          location: "Iowa",
          status: "NOT_RUNNING"
        }
      ]
    }
  }
}

Your component can access that array through props.config.component.data and use it to render a grid, card list, or detail view.

For example, render the fetched rows in a custom component

TypeScript
/* {package}/src/ui/components/WindTurbineGrid.tsx */

import * as React from 'react';

const WindTurbineGrid = (props) => {
  const turbines = props.config.component.data || [];

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Location</th>
          <th>Status</th>
        </tr>
      </thead>
      <tbody>
        {turbines.map((turbine) => (
          <tr key={turbine.id}>
            <td>{turbine.name}</td>
            <td>{turbine.location}</td>
            <td>{turbine.status}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default WindTurbineGrid;

This rendering logic gives users a view of the structured records in a familiar tabular format.

To connect these concepts together:

  • Define @uiSdlDataSpec(dataDestinationField='data') to store the fetch result in data
  • Build the fetch in toPartiallyAppliedActions()
  • The platform runs that fetch and injects the result
  • The component now renders the data—without writing Redux, REST calls, or boilerplate code

Step 5: Create a JSON configuration

This JSON configuration instantiates your component in the application UI. It also defines the entity to query and which fields to display. The platform uses this metadata to connect everything together.

JSON
// {package}/ui/c3/meta/WindTurbine.TranslationGrid.json

{
  "type": "UiSdlConnected<MyCustomGrid>",              // Use your custom component Type
  "component": {
    "dataSpec": {
      "dataType": "Translation",                       // Fetch records from the Translation entity Type
      "columnFields": ["id", "key", "value"]           // Display these fields in the grid
    }
  }
}

This configuration:

  • Loads records from the Translation entity
  • Selects the id, key, and value fields
  • Injects the fetched records into props.config.component.data
  • Renders those records using the .tsx component you defined in earlier steps

This example is minimal, but you can add filters, sorting, pagination, or UI overrides just like you would for any other platform-connected component.

The key difference is: this one uses your custom logic and your data layout — but the platform still takes care of the data fetch.

Step 6: Support platform features

When your component uses UiSdlFieldBasedDataSpec, the platform can enhance it with built-in features. These examples show how to enable pagination, sorting, and filtering.

Pagination

This configuration sets up pagination by specifying a page size (limit) and starting page index (page). The platform manages the fetch offset internally.

JSON
// {package}/ui/c3/meta/WindTurbine.GridWithPagination.json

{
  "type": "UiSdlConnected<MyCustomGrid>",
  "component": {
    "dataSpec": {
      "dataType": "WindTurbine",                 // Backend Type to query
      "columnFields": ["id", "name", "status"],  // Fields to display
      "limit": 10,                                // Show 10 records per page
      "page": 0                                   // Start on the first page
    }
  }
}

Sorting

Add the sortable flag to a column field to let users sort rows by that column. The platform updates the query based on user interaction.

JSON
// {package}/ui/c3/meta/WindTurbine.SortableGrid.json

{
  "type": "UiSdlConnected<UiSdlDataGrid>",
  "component": {
    "dataSpec": {
      "dataType": "WindTurbine",
      "columnFields": [
        {
          "fieldName": "name",
          "label": "Turbine Name",
          "sortable": true                       // Enable sorting on this column
        },
        {
          "fieldName": "status",
          "label": "Status"
        }
      ]
    }
  }
}

Filtering

Combine a UiSdlFilterPanel with a filterFields configuration to enable user-driven filters. The platform links the filter input to the backend query automatically.

JSON
// {package}/ui/c3/meta/WindTurbine.FilterableGridPage.json

{
  "type": "UiSdlConnected<UiSdlLayoutNavMenu>",
  "component": {
    "children": [
      {
        "type": "UiSdlFilterPanel",              // Show filtering controls
        "component": {
          "fields": [
            {
              "fieldName": "status",             // Filter by status (e.g., RUNNING, NOT_RUNNING)
              "label": "Status"
            }
          ]
        }
      },
      {
        "id": "WindTurbine.FilterableGrid"       // Must match the ID of the component to filter
      }
    ]
  }
}
JSON
// {package}/ui/c3/meta/WindTurbine.FilterableGrid.json

{
  "id": "WindTurbine.FilterableGrid",
  "type": "UiSdlConnected<MyCustomGrid>",
  "component": {
    "dataSpec": {
      "dataType": "WindTurbine",
      "columnFields": ["id", "name", "status"],
      "filterFields": ["status"]                 // Enable filtering by the 'status' field
    }
  }
}
Was this page helpful?