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 Path | Description |
|---|---|
{package}/src/ui/components/MyCustomGrid.c3typ | Defines the component |
{package}/src/ui/components/MyCustomGridDataSpec.c3typ | Describes how to fetch records |
{package}/src/ui/components/MyCustomGridDataSpec.ts | Implements the fetch logic for records |
{package}/src/ui/components/WindTurbineGrid.tsx | Renders the fetched records as a table |
{package}/ui/c3/meta/WindTurbine.TranslationGrid.json | Declares 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:
{
"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:
{
"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.
// {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.
// {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.
When your component loads, the platform calls toPartiallyAppliedActions() to ask: “What data should I load for this component, and how should I call the backend?”
// {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:
{
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
/* {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 indata - 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.
// {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
Translationentity - Selects the
id,key, andvaluefields - Injects the fetched records into
props.config.component.data - Renders those records using the
.tsxcomponent 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.
// {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.
// {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.
// {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
}
]
}
}// {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
}
}
}