Custom widgets can use the built-in Studio form builder to edit their data and format settings.
"use client";
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgStudio, AgStudioRef } from "ag-studio-react";
import {
AgDataEngine,
AgDataSourcesDefinition,
AgDefaultRegistry,
AgReportState,
AgStudioApi,
AgStudioApiReadyEvent,
AgStudioMode,
AgStudioProperties,
AgWidgetFormParams,
AgWidgetsConfig,
createWidgets,
} from "ag-studio";
import { CustomDef, MyRegistry } from "./interfaces.tsx";
import CustomWidget from "./customWidget.tsx";
const StudioExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const studioStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [data, setData] = useState<AgDataSourcesDefinition | AgDataEngine>();
const initialState = useMemo<AgReportState<MyRegistry>>(() => {
return {
pages: [
{
id: "page1",
widgets: {
"1": {
type: "customWidget",
dataMapping: {
value: [
{
id: "medals.gold",
aggregation: "sum",
},
],
},
},
},
widgetLayout: {
"1": {
xTrack: 0,
yTrack: 0,
xSpan: 24,
ySpan: 12,
},
},
selection: {
type: "widget",
id: "1",
},
},
],
selectedPageId: "page1",
panels: {
filters: {
collapsed: true,
},
data: {
collapsed: true,
},
},
};
}, []);
const widgets = useMemo<
| AgWidgetsConfig<MyRegistry>
| ((
widgets: AgWidgetsConfig<AgDefaultRegistry>,
) => AgWidgetsConfig<MyRegistry>)
>(() => {
return createWidgets<MyRegistry>({
additionalTypes: [
{
id: "customWidget",
icon: {
url: "https://www.ag-grid.com/studio/images/brandmark.svg",
},
label: "Custom Widget",
dataMapping: {
value: {
type: "field",
supportedRoles: ["numeric"],
requires: { cardinality: "one" },
required: true,
},
},
form: (params: AgWidgetFormParams<CustomDef>) => {
const defaultForm = params.createDefaults({
dataMappingItems: [
{
key: "value",
label: "Value",
},
],
});
defaultForm.items[0].items.push({
type: "section",
key: "customSection",
label: "Special Config",
items: [
{
type: "number",
id: "format.style.valueFontSize",
label: "Value Font Size",
defaultValue: 48,
},
],
});
return defaultForm;
},
comp: CustomWidget,
defaultSize: {
width: 400,
height: 300,
},
minSize: {
width: 200,
height: 100,
},
ai: {
description:
"Custom widget used for displaying values in an interesting way.",
},
},
],
menu: [
{
label: "Custom",
widgetIds: ["customWidget"],
},
],
});
}, []);
const onApiReady = useCallback((params: AgStudioApiReadyEvent) => {
fetch("https://www.ag-grid.com/studio/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: any[]) => setData({ sources: [{ id: "medals", data }] }));
}, []);
return (
<div style={containerStyle}>
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<AgStudio<MyRegistry>
style={studioStyle}
className="my-studio-container"
data={data}
initialState={initialState}
widgets={widgets}
mode={"edit"}
onApiReady={onApiReady}
/>
</div>
</div>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<StudioExample />
</StrictMode>,
);
.custom-widget-container {
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
}
.custom-widget-desc {
margin: 16px;
font-size: 24px;
align-content: center;
}
.custom-widget-value {
margin: 16px;
border: 2px solid var(--ag-border-color);
border-radius: var(--ag-border-radius);
text-align: center;
align-content: center;
font-weight: bold;
font-size: var(--custom-widget-value-font-size, 48px);
}
importScripts('https://cdn.jsdelivr.net/npm/typescript@5.4.5/lib/typescript.min.js');
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
async function transpile(request, ext) {
const response = await fetch(request);
if (!response.ok) return response;
const source = await response.text();
const result = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
jsx: ext.endsWith('x') ? ts.JsxEmit.React : undefined,
experimentalDecorators: ext === 'ts',
emitDecoratorMetadata: ext === 'ts',
},
});
return new Response(result.outputText, {
headers: { 'Content-Type': 'application/javascript' },
});
}
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
const ext = url.pathname
.match(/\.([a-z0-9]+)$/i)
?.at(1)
?.toLowerCase();
if (['jsx', 'ts', 'tsx'].includes(ext)) {
event.respondWith(transpile(event.request, ext));
}
});
import type { AgBaseRegistry, AgBaseWidgetDefinition, AgWidgetDataFormat, AgWidgetFieldReference } from 'ag-studio';
interface CustomWidgetStyle {
valueFontSize?: number;
}
export interface CustomDef {
type: 'customWidget';
dataMapping: {
value: AgWidgetFieldReference[];
};
format?: AgWidgetDataFormat<CustomWidgetStyle>;
}
export interface MyRegistry extends AgBaseRegistry {
widgets: readonly AgBaseWidgetDefinition<'customWidget', CustomDef>[];
}
import type { AgWidgetParams } from 'ag-studio';
import type { CSSProperties } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import type { CustomDef } from './interfaces';
export default ({ dataMapping, widgetApi, format }: AgWidgetParams<CustomDef>) => {
const [value, setValue] = useState<string>();
const hasLoaded = useRef(false);
const valueFontSize = format?.style?.valueFontSize;
useEffect(() => {
const fetchData = async () => {
const field = dataMapping.value?.at(0);
if (!field) {
widgetApi.setDisplayState('incompleteDataMapping');
return;
}
widgetApi.setDisplayState('loading', { prominent: !hasLoaded.current });
const response = await widgetApi.getData({ fields: [field] });
const data = response.results.rows;
const newValue = data.at(0)?.[field.key];
const hasData = newValue != null;
hasLoaded.current = hasData;
setValue(widgetApi.formatFieldValue(field, newValue));
widgetApi.setDisplayState(hasData ? 'displayed' : 'noData');
};
fetchData();
}, [dataMapping]);
return (
<div
className="custom-widget-container"
style={{ '--custom-widget-value-font-size': valueFontSize ? `${valueFontSize}px` : null } as CSSProperties}
>
<div className="custom-widget-desc">Custom widget</div>
<div className="custom-widget-value">{value ?? ''}</div>
</div>
);
};
The example above adds a Special Config section to the setup tab, with an input to control the font size of the value in the custom widget.
The form is provided via the form property of the widget definition.
Form configuration using typed form builder.
|
Form Setup Copy Link
The ID of each form item is a dot delimited path of the corresponding property within the widget config.
interface CustomWidgetStyle {
valueFontSize?: number;
}
interface CustomWidgetDef {
type: 'customWidget';
dataMapping: {
value: AgWidgetFieldReference[];
};
format?: AgWidgetDataFormat<CustomWidgetStyle>;
}For the above definition, the corresponding input ID for the valueFontSize property would be format.style.valueFontSize.
Form Helpers Copy Link
The form callback provides some helper functions to create form elements for the default properties (e.g. data mapping, titles, etc.).
const widgetDefinition = {
// ...
form: (params) => {
return params.createDefaults({
dataMappingItems: [
{
key: 'value',
label: 'Value',
},
],
});
},
} Create the default tab group with a setup and format tab. Setup tab contains: |
Creates a section containing the widget type selector.
|
Create the data mapping item. Either a section if multiple data mapping fields, or a single fieldset or field item.
|
Creates a section containing the cross filter input.
|
Creates the title group.
|
Creates the subtitle group.
|
Creates the caption group.
|
Creates the title section containing the title, subtitle and caption group.
|
Form Grouping Items Copy Link
The following form items allow for grouping/structuring the form items (e.g. they have children).
| Item | Interface | Description |
|---|---|---|
| Tab Group | AgWidgetFormTabGroup / AgFormTabGroup | A group of tab items. E.g. the top-level Setup / Format tab group in the edit panel for the default widgets. |
| Tab | AgWidgetFormTab / AgFormTab | A child tab of a tab group item. E.g. the Setup tab in the edit panel for the default widgets. |
| Section | AgWidgetFormSection / AgFormSection | A top-level collection of items. E.g. the Titles section in the edit panel for the default widgets. |
| Group | AgWidgetFormGroup / AgFormGroup | A lower-level collection of items (with an optional toggle). E.g. the Title group in the edit panel for the default widgets. |
Form Input Items Copy Link
| Item | Interface | Description |
|---|---|---|
| Select | AgWidgetFormSelect / AgFormSelect | A select input item. |
| Checkbox | AgWidgetFormCheckbox / AgFormCheckbox | A checkbox input item. |
| Toggle | AgWidgetFormToggle / AgFormToggle | A toggle input item. |
| Text Input | AgWidgetFormTextField / AgFormTextField | A text input item. |
| Text Area | AgWidgetFormTextArea / AgFormTextArea | A text area input item. |
| Number Input | AgWidgetFormNumber / AgFormNumber | A number input item. |
| Optional Number Input | AgWidgetFormOptionalNumber / AgFormOptionalNumber | A number input item that allows optional values. |
| Color Input | AgWidgetFormColor / AgFormColor | A color picker input item. |
| Widget Type Selector | AgWidgetFormWidgetType (widget form only) | A select input that allows changing the widget type. |
| Field Selection Input | AgWidgetFormField (widget form only) | An input for selecting a field (supporting drag and drop). |
| Fieldset Selection Input | AgWidgetFormFieldSet (widget form only) | An input for selecting multiple fields (supporting drag and drop). |
| Grouped Typography Input | AgWidgetFormTypography (widget form only) | A group of elements for configuring typography. |