@react-typed-forms/schemas 16.2.3 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORM_EXTENSIONS_GUIDE.md +781 -0
- package/lib/RenderForm.d.ts +22 -5
- package/lib/controlBuilder.d.ts +4 -47
- package/lib/controlRender.d.ts +49 -24
- package/lib/index.cjs +310 -332
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +270 -272
- package/lib/index.js.map +1 -1
- package/lib/renderer/elementSelected.d.ts +8 -0
- package/lib/renderers.d.ts +6 -2
- package/lib/types.d.ts +10 -5
- package/lib/util.d.ts +3 -2
- package/package.json +5 -5
- package/src/RenderForm.tsx +130 -64
- package/src/controlBuilder.ts +6 -193
- package/src/controlRender.tsx +127 -81
- package/src/createFormRenderer.tsx +52 -19
- package/src/index.ts +1 -0
- package/src/renderer/elementSelected.ts +48 -0
- package/src/renderers.tsx +8 -1
- package/src/types.ts +15 -5
- package/src/util.ts +13 -1
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
# Form Extensions Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document describes how to create custom form extensions in the ServiceTas application. Form extensions allow you to add custom renderers for inputs, groups, adornments, labels, and display components that integrate seamlessly with the AppForms system.
|
|
6
|
+
|
|
7
|
+
## Extension Types
|
|
8
|
+
|
|
9
|
+
The form rendering system supports five types of extensions:
|
|
10
|
+
|
|
11
|
+
1. **Data Renderers** - Custom input/data entry controls
|
|
12
|
+
2. **Group Renderers** - Custom layouts for groups of fields
|
|
13
|
+
3. **Adornment Renderers** - Decorative or functional elements added to controls
|
|
14
|
+
4. **Label Renderers** - Custom rendering for field labels
|
|
15
|
+
5. **Display Renderers** - Custom read-only display components
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
Form extensions consist of three main parts:
|
|
20
|
+
|
|
21
|
+
1. **Extension Definition** (`formExtensions.ts`) - Defines configuration options
|
|
22
|
+
2. **Renderer Implementation** (`renderer.tsx` or separate files) - Implements the visual component
|
|
23
|
+
3. **Registration** (`renderer.tsx`) - Registers the renderer with the form system
|
|
24
|
+
|
|
25
|
+
## Creating a Form Extension
|
|
26
|
+
|
|
27
|
+
### Step 1: Define Extension Configuration
|
|
28
|
+
|
|
29
|
+
Create a `CustomRenderOptions` object in `formExtensions.ts`:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
buildSchema,
|
|
34
|
+
CustomRenderOptions,
|
|
35
|
+
boolField,
|
|
36
|
+
stringField,
|
|
37
|
+
intField,
|
|
38
|
+
stringOptionsField
|
|
39
|
+
} from "@react-typed-forms/schemas";
|
|
40
|
+
|
|
41
|
+
// Define the configuration interface
|
|
42
|
+
export interface MyExtensionOptions {
|
|
43
|
+
displayLabel: boolean;
|
|
44
|
+
customColor?: string;
|
|
45
|
+
size?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create the extension definition
|
|
49
|
+
export const MyExtensionOptions: CustomRenderOptions = {
|
|
50
|
+
value: "MyExtension", // Unique identifier (used in renderType)
|
|
51
|
+
name: "My Extension", // Display name (shown in form editor)
|
|
52
|
+
fields: buildSchema<MyExtensionOptions>({
|
|
53
|
+
displayLabel: boolField("Display Label"),
|
|
54
|
+
customColor: stringField("Custom Color"),
|
|
55
|
+
size: intField("Size")
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Field Types Available:**
|
|
61
|
+
- `boolField(label)` - Boolean checkbox
|
|
62
|
+
- `stringField(label)` - Text input
|
|
63
|
+
- `intField(label)` - Numeric input
|
|
64
|
+
- `stringOptionsField(label, ...options)` - Dropdown selector
|
|
65
|
+
- Custom field types from `@react-typed-forms/schemas`
|
|
66
|
+
|
|
67
|
+
**Important Notes:**
|
|
68
|
+
- `value` must be unique across all extensions
|
|
69
|
+
- `fields` is optional - use empty array `[]` if no configuration needed
|
|
70
|
+
- For extensions that extend existing types (like `DataRenderType.Dropdown`), use the existing type as the `value`
|
|
71
|
+
|
|
72
|
+
### Step 2: Create the Renderer Component
|
|
73
|
+
|
|
74
|
+
#### Data Renderer (Input Controls)
|
|
75
|
+
|
|
76
|
+
Data renderers are for custom input controls. Examples: Switch, AddressFinder, Chart, Map.
|
|
77
|
+
|
|
78
|
+
**File location:** `renderer.tsx` or `renderer/MyControl.tsx`
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { createDataRenderer } from "@react-typed-forms/schemas";
|
|
82
|
+
import { MyExtensionOptions } from "./formExtensions";
|
|
83
|
+
|
|
84
|
+
export const MyDataRenderer = createDataRenderer(
|
|
85
|
+
(props, renderer) => {
|
|
86
|
+
const { control, renderOptions, className, id } = props;
|
|
87
|
+
const options = renderOptions as MyExtensionOptions & RenderOptions;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className={className}>
|
|
91
|
+
<input
|
|
92
|
+
type="text"
|
|
93
|
+
value={control.value ?? ''}
|
|
94
|
+
onChange={(e) => control.setValue(e.target.value)}
|
|
95
|
+
disabled={control.disabled}
|
|
96
|
+
/>
|
|
97
|
+
{options.displayLabel && <span>{control.value}</span>}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
renderType: MyExtensionOptions.value, // Must match the value from Step 1
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Key Props Available:**
|
|
108
|
+
- `control` - The form control (has `.value`, `.setValue()`, `.disabled`, `.fields`)
|
|
109
|
+
- `renderOptions` - Configuration options from the extension definition
|
|
110
|
+
- `className` - CSS classes to apply
|
|
111
|
+
- `id` - Unique identifier for the field
|
|
112
|
+
- `dataNode` - Access to parent/child nodes in the form tree
|
|
113
|
+
- `renderer` - Access to the FormRenderer for rendering nested elements
|
|
114
|
+
|
|
115
|
+
**Example: Switch Renderer (from renderer.tsx:94-121)**
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const SwitchRenderer = createDataRenderer(
|
|
119
|
+
(props, renderer) => {
|
|
120
|
+
const { renderOptions, control } = props;
|
|
121
|
+
const { displayLabel } = renderOptions as ExtendedSwitch & RenderOptions;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<label className="inline-flex items-center cursor-pointer">
|
|
125
|
+
<input
|
|
126
|
+
type="checkbox"
|
|
127
|
+
checked={control.value ?? false}
|
|
128
|
+
onChange={() => control.setValue((x) => !x)}
|
|
129
|
+
className="sr-only peer"
|
|
130
|
+
disabled={control.disabled}
|
|
131
|
+
/>
|
|
132
|
+
<div className="..." />
|
|
133
|
+
{displayLabel && (
|
|
134
|
+
<span className="ms-3 subhead">
|
|
135
|
+
{control.value ?? false ? "On" : "Off"}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</label>
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
renderType: SwitchOptions.value,
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Group Renderer (Layout Components)
|
|
148
|
+
|
|
149
|
+
Group renderers control how groups of fields are laid out. Example: TopLevelGroup.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { createGroupRenderer, GroupRendererProps } from "@react-typed-forms/schemas";
|
|
153
|
+
|
|
154
|
+
export const MyGroupRenderer = createGroupRenderer(
|
|
155
|
+
(props, renderers) => {
|
|
156
|
+
const { className, style, definition, dataContext, renderChild, formNode } = props;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className={className} style={style}>
|
|
160
|
+
{formNode.children.map((child, i) => renderChild(child))}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
renderType: MyGroupOptions.value,
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Key Props Available:**
|
|
171
|
+
- `definition` - The group definition
|
|
172
|
+
- `dataContext` - Data context including parentNode
|
|
173
|
+
- `renderChild` - Function to render child fields
|
|
174
|
+
- `formNode` - The form node with children array
|
|
175
|
+
- `className`, `style` - Styling properties
|
|
176
|
+
|
|
177
|
+
**Example: TopLevelGroup Renderer (from renderer.tsx:176-213)**
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const topLevelGroupRenderer = createGroupRenderer(
|
|
181
|
+
(p, renderers) => (
|
|
182
|
+
<div className={rendererClass(className, DefaultRenderOptions.group?.standardClassName)}>
|
|
183
|
+
<AllErrors
|
|
184
|
+
definition={definition}
|
|
185
|
+
dataNode={dataContext.parentNode}
|
|
186
|
+
labelRenderer={renderers.renderLabelText}
|
|
187
|
+
/>
|
|
188
|
+
{formNode.children.map((c, i) => renderChild(c))}
|
|
189
|
+
</div>
|
|
190
|
+
),
|
|
191
|
+
{
|
|
192
|
+
renderType: TopLevelGroupOption.value,
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Adornment Renderer (Decorations)
|
|
198
|
+
|
|
199
|
+
Adornment renderers add decorative or functional elements to controls. Examples: HelpText, Tooltip.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import {
|
|
203
|
+
createAdornmentRenderer,
|
|
204
|
+
wrapMarkup,
|
|
205
|
+
appendMarkupAt,
|
|
206
|
+
AdornmentPlacement,
|
|
207
|
+
ControlAdornment
|
|
208
|
+
} from "@react-typed-forms/schemas";
|
|
209
|
+
|
|
210
|
+
export const MyAdornmentRenderer = createAdornmentRenderer(
|
|
211
|
+
(props, renderers) => {
|
|
212
|
+
const options = props.adornment as MyAdornmentOptions & ControlAdornment;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
apply: wrapMarkup("children", (children) => (
|
|
216
|
+
<div className="relative">
|
|
217
|
+
{children}
|
|
218
|
+
<span className="adornment">{options.adornmentText}</span>
|
|
219
|
+
</div>
|
|
220
|
+
)),
|
|
221
|
+
priority: 0, // Lower numbers render first (outer layers)
|
|
222
|
+
adornment: props.adornment,
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
adornmentType: ControlAdornmentType.MyAdornment,
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Markup Manipulation Functions:**
|
|
232
|
+
- `wrapMarkup(target, wrapper)` - Wraps the target element
|
|
233
|
+
- Targets: `"children"`, `"label"`, `"control"`, etc.
|
|
234
|
+
- `appendMarkupAt(placement, element)` - Adds element at specific position
|
|
235
|
+
- Placements: `AdornmentPlacement.LabelEnd`, `AdornmentPlacement.ControlEnd`, etc.
|
|
236
|
+
|
|
237
|
+
**Example: HelpText Renderer (from renderer.tsx:237-282)**
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const createHelpTextRenderer = (container: HTMLElement | null) => {
|
|
241
|
+
return createAdornmentRenderer(
|
|
242
|
+
(p, renderers) => {
|
|
243
|
+
const label = (p.adornment as ExtendedHelpText).helpLabel;
|
|
244
|
+
const helpText = (p.adornment as HelpTextAdornment).helpText;
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
apply: appendMarkupAt(
|
|
248
|
+
(p.adornment as HelpTextAdornment).placement ?? AdornmentPlacement.LabelEnd,
|
|
249
|
+
<Popover.Root>
|
|
250
|
+
<Popover.Trigger asChild>
|
|
251
|
+
<button className="...">
|
|
252
|
+
<i className="fa fa-info-circle mr-2" />
|
|
253
|
+
{renderers.renderLabelText(label)}
|
|
254
|
+
</button>
|
|
255
|
+
</Popover.Trigger>
|
|
256
|
+
<Popover.Portal container={container}>
|
|
257
|
+
<Popover.Content className="...">
|
|
258
|
+
<Div className="..." html={helpText} />
|
|
259
|
+
</Popover.Content>
|
|
260
|
+
</Popover.Portal>
|
|
261
|
+
</Popover.Root>
|
|
262
|
+
),
|
|
263
|
+
priority: 0,
|
|
264
|
+
adornment: p.adornment,
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
adornmentType: ControlAdornmentType.HelpText,
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### Label Renderer (Custom Labels)
|
|
275
|
+
|
|
276
|
+
Label renderers customize how field labels are displayed. Example: HtmlLabel.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { createLabelRenderer, LabelType } from "@react-typed-forms/schemas";
|
|
280
|
+
|
|
281
|
+
export const MyLabelRenderer = createLabelRenderer(
|
|
282
|
+
(props) => {
|
|
283
|
+
return <span className="custom-label">{props.label}</span>;
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
labelType: LabelType.Text,
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Example: Html Label Renderer (from renderer.tsx:159-174)**
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const HtmlLabelRenderer = createLabelRenderer(
|
|
295
|
+
(p) => <HtmlLabel label={p.label} />,
|
|
296
|
+
{ labelType: LabelType.Text }
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
function HtmlLabel({ label }: { label: ReactNode }) {
|
|
300
|
+
const labelText = useMemo(() => {
|
|
301
|
+
if (typeof label === "string") {
|
|
302
|
+
return parse(label); // Parse HTML string
|
|
303
|
+
}
|
|
304
|
+
return label;
|
|
305
|
+
}, [label]);
|
|
306
|
+
return labelText;
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### Display Renderer (Read-only Display)
|
|
311
|
+
|
|
312
|
+
Display renderers show read-only data. Example: Html display.
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import {
|
|
316
|
+
createDisplayRenderer,
|
|
317
|
+
DisplayDataType,
|
|
318
|
+
HtmlDisplay
|
|
319
|
+
} from "@react-typed-forms/schemas";
|
|
320
|
+
|
|
321
|
+
export const MyDisplayRenderer = createDisplayRenderer(
|
|
322
|
+
(props) => {
|
|
323
|
+
const data = props.data as MyDisplayData;
|
|
324
|
+
return <div className="display">{data.content}</div>;
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
renderType: DisplayDataType.MyDisplay,
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Example: Html Display Renderer (from renderer.tsx:123-138)**
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
export function createHtmlRenderer(
|
|
336
|
+
makeOnClick: (actionId: string, data: any) => () => void
|
|
337
|
+
) {
|
|
338
|
+
return createDisplayRenderer(
|
|
339
|
+
(props) => {
|
|
340
|
+
return (
|
|
341
|
+
<HtmlDisplayRenderer
|
|
342
|
+
{...props}
|
|
343
|
+
html={(props.data as HtmlDisplay).html ?? ""}
|
|
344
|
+
makeOnClick={makeOnClick}
|
|
345
|
+
/>
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
{ renderType: DisplayDataType.Html }
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Step 3: Register the Extension
|
|
354
|
+
|
|
355
|
+
#### Register in EditorExtension
|
|
356
|
+
|
|
357
|
+
Add your extension to `EditorExtension` in `formExtensions.ts`:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
export const EditorExtension: ControlDefinitionExtension = {
|
|
361
|
+
GroupRenderOptions: TopLevelGroupOption, // Single group renderer (optional)
|
|
362
|
+
ControlAdornment: [ // Array of adornment options
|
|
363
|
+
HelpTextOptions,
|
|
364
|
+
SpotlightOptions,
|
|
365
|
+
MyAdornmentOptions // Add your adornment here
|
|
366
|
+
],
|
|
367
|
+
RenderOptions: [ // Array of data/display renderers
|
|
368
|
+
MapOptions,
|
|
369
|
+
ChartDefinition,
|
|
370
|
+
SwitchOptions,
|
|
371
|
+
MyExtensionOptions // Add your data renderer here
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Extension Categories:**
|
|
377
|
+
- `GroupRenderOptions` - Single group renderer option
|
|
378
|
+
- `ControlAdornment` - Array of adornment options
|
|
379
|
+
- `RenderOptions` - Array of data renderer options
|
|
380
|
+
|
|
381
|
+
#### Register the Renderer
|
|
382
|
+
|
|
383
|
+
Add your renderer to the `createStdRenderer` function in `renderer.tsx`:
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
export function createStdRenderer(
|
|
387
|
+
defaults: DefaultRendererOptions,
|
|
388
|
+
options: StdRenderOptions,
|
|
389
|
+
...others: RendererRegistration[]
|
|
390
|
+
) {
|
|
391
|
+
return createFormRenderer(
|
|
392
|
+
[
|
|
393
|
+
...others,
|
|
394
|
+
HtmlLabelRenderer,
|
|
395
|
+
MapRenderer,
|
|
396
|
+
SwitchRenderer,
|
|
397
|
+
MyDataRenderer, // Add your renderer here
|
|
398
|
+
MyAdornmentRenderer(options.container),
|
|
399
|
+
topLevelGroupRenderer,
|
|
400
|
+
// ... other renderers
|
|
401
|
+
],
|
|
402
|
+
createDefaultRenderers(defaults)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Registration Order:**
|
|
408
|
+
- Renderers are processed in order
|
|
409
|
+
- More specific renderers should come before more general ones
|
|
410
|
+
- Adornments with lower priority numbers render first (outer layers)
|
|
411
|
+
|
|
412
|
+
## Complete Examples
|
|
413
|
+
|
|
414
|
+
### Example 1: Simple Switch Control
|
|
415
|
+
|
|
416
|
+
**formExtensions.ts:**
|
|
417
|
+
```typescript
|
|
418
|
+
export interface ExtendedSwitch {
|
|
419
|
+
displayLabel: boolean;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export const SwitchOptions: CustomRenderOptions = {
|
|
423
|
+
name: "Switch",
|
|
424
|
+
value: "Switch",
|
|
425
|
+
fields: buildSchema<ExtendedSwitch>({
|
|
426
|
+
displayLabel: boolField("Display Label"),
|
|
427
|
+
}),
|
|
428
|
+
};
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**renderer.tsx:**
|
|
432
|
+
```typescript
|
|
433
|
+
const SwitchRenderer = createDataRenderer(
|
|
434
|
+
(props, renderer) => {
|
|
435
|
+
const { renderOptions, control } = props;
|
|
436
|
+
const { displayLabel } = renderOptions as ExtendedSwitch & RenderOptions;
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<label className="inline-flex items-center cursor-pointer">
|
|
440
|
+
<input
|
|
441
|
+
type="checkbox"
|
|
442
|
+
checked={control.value ?? false}
|
|
443
|
+
onChange={() => control.setValue((x) => !x)}
|
|
444
|
+
className="sr-only peer"
|
|
445
|
+
disabled={control.disabled}
|
|
446
|
+
/>
|
|
447
|
+
<div className="relative w-11 h-6 bg-[#e4e4e7] peer-checked:bg-accent ..." />
|
|
448
|
+
{displayLabel && (
|
|
449
|
+
<span className="ms-3 subhead">
|
|
450
|
+
{control.value ?? false ? "On" : "Off"}
|
|
451
|
+
</span>
|
|
452
|
+
)}
|
|
453
|
+
</label>
|
|
454
|
+
);
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
renderType: SwitchOptions.value,
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Example 2: Chart Renderer with Configuration
|
|
463
|
+
|
|
464
|
+
**ChartRendererConfigs.ts:**
|
|
465
|
+
```typescript
|
|
466
|
+
export enum ChartType {
|
|
467
|
+
Doughnut = "Doughnut",
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export interface ChartOptions {
|
|
471
|
+
chartType?: string;
|
|
472
|
+
ringColor?: string;
|
|
473
|
+
ringBackgroundColor?: string;
|
|
474
|
+
displayText?: boolean;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const ChartFields = buildSchema<ChartOptions>({
|
|
478
|
+
chartType: stringOptionsField("Type", {
|
|
479
|
+
name: "Doughnut",
|
|
480
|
+
value: ChartType.Doughnut,
|
|
481
|
+
}),
|
|
482
|
+
ringColor: stringField("Ring Color"),
|
|
483
|
+
ringBackgroundColor: stringField("Ring Background Color"),
|
|
484
|
+
displayText: boolField("Display Text"),
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
export const ChartDefinition: CustomRenderOptions = {
|
|
488
|
+
name: "Chart",
|
|
489
|
+
value: "Chart",
|
|
490
|
+
fields: ChartFields,
|
|
491
|
+
};
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**ChartRenderer.tsx:**
|
|
495
|
+
```typescript
|
|
496
|
+
export function createChartRenderer(options: ChartOptions = {}) {
|
|
497
|
+
return createDataRenderer(
|
|
498
|
+
(p) => {
|
|
499
|
+
const { id, renderOptions, dataNode } = p;
|
|
500
|
+
const chartOptions = mergeObjects(
|
|
501
|
+
renderOptions as ChartOptions & RenderOptions,
|
|
502
|
+
options
|
|
503
|
+
) ?? {};
|
|
504
|
+
|
|
505
|
+
const { chartType } = chartOptions;
|
|
506
|
+
const fields = dataNode.control.fields;
|
|
507
|
+
|
|
508
|
+
switch (chartType) {
|
|
509
|
+
case ChartType.Doughnut:
|
|
510
|
+
return (
|
|
511
|
+
<DoughnutChart
|
|
512
|
+
key={id}
|
|
513
|
+
progress={fields.activePoints as Control<number>}
|
|
514
|
+
total={fields.totalPoints as Control<number>}
|
|
515
|
+
ringBackgroundColor={chartOptions.ringBackgroundColor}
|
|
516
|
+
ringColor={chartOptions.ringColor}
|
|
517
|
+
displayText={chartOptions.displayText ?? false}
|
|
518
|
+
/>
|
|
519
|
+
);
|
|
520
|
+
default:
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
renderType: ChartDefinition.value,
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Example 3: Address Finder with External Service
|
|
532
|
+
|
|
533
|
+
**formExtensions.ts:**
|
|
534
|
+
```typescript
|
|
535
|
+
export interface AddressExtraOptions {
|
|
536
|
+
disablePostalAddress?: boolean;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export const AddressFinderOptions: CustomRenderOptions = {
|
|
540
|
+
name: "AddressFinder",
|
|
541
|
+
value: "AddressFinder",
|
|
542
|
+
fields: buildSchema<AddressExtraOptions>({
|
|
543
|
+
disablePostalAddress: boolField("Disable Postal Address"),
|
|
544
|
+
}),
|
|
545
|
+
};
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**AddressFinderControl.tsx:**
|
|
549
|
+
```typescript
|
|
550
|
+
export function createAddressFinderRenderer() {
|
|
551
|
+
return createDataRenderer(
|
|
552
|
+
(dataProps, renderer) => (
|
|
553
|
+
<AddressFinderRenderer renderer={renderer} dataProps={dataProps} />
|
|
554
|
+
),
|
|
555
|
+
{
|
|
556
|
+
renderType: AddressFinderOptions.value,
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function AddressFinderRenderer({
|
|
562
|
+
renderer,
|
|
563
|
+
dataProps,
|
|
564
|
+
}: {
|
|
565
|
+
dataProps: DataRendererProps;
|
|
566
|
+
renderer: FormRenderer;
|
|
567
|
+
}) {
|
|
568
|
+
const {
|
|
569
|
+
addressSuggestions,
|
|
570
|
+
addressSearchControl,
|
|
571
|
+
getSuggestions,
|
|
572
|
+
selectedAddress,
|
|
573
|
+
} = useAddressFinder({
|
|
574
|
+
dataProps,
|
|
575
|
+
key: "API_KEY_HERE",
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<AutocompleteInput
|
|
580
|
+
options={addressSuggestions.value}
|
|
581
|
+
getOptionText={(x) => x.fullAddress}
|
|
582
|
+
selectedControl={selectedAddress}
|
|
583
|
+
textControl={addressSearchControl as Control<string>}
|
|
584
|
+
onInputChange={(_, v) => {
|
|
585
|
+
addressSearchControl.value = v;
|
|
586
|
+
getSuggestions(v);
|
|
587
|
+
}}
|
|
588
|
+
/>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
## Best Practices
|
|
594
|
+
|
|
595
|
+
### 1. Naming Conventions
|
|
596
|
+
- Use descriptive names: `SwitchRenderer`, `ChartRenderer`, not `CustomRenderer1`
|
|
597
|
+
- Interface names should end with "Options": `ChartOptions`, `SwitchOptions`
|
|
598
|
+
- Renderer constants should end with "Renderer": `MapRenderer`, `SwitchRenderer`
|
|
599
|
+
|
|
600
|
+
### 2. Configuration Design
|
|
601
|
+
- Only add configuration options that are truly needed
|
|
602
|
+
- Use appropriate field types for the data (bool, string, int, options)
|
|
603
|
+
- Provide sensible defaults in the renderer implementation
|
|
604
|
+
- Use empty `fields: []` if no configuration is needed
|
|
605
|
+
|
|
606
|
+
### 3. Component Structure
|
|
607
|
+
- Keep renderer logic minimal - delegate to separate components
|
|
608
|
+
- Use hooks for complex logic (see `useAddressFinder`)
|
|
609
|
+
- Handle undefined/null values gracefully
|
|
610
|
+
- Always respect `control.disabled` state
|
|
611
|
+
|
|
612
|
+
### 4. Styling
|
|
613
|
+
- Use `rendererClass()` to merge CSS classes properly
|
|
614
|
+
- Respect the `className` prop passed to your renderer
|
|
615
|
+
- Use Tailwind classes for consistency
|
|
616
|
+
- Consider responsive design (mobile vs desktop)
|
|
617
|
+
|
|
618
|
+
### 5. Accessibility
|
|
619
|
+
- Add proper ARIA attributes
|
|
620
|
+
- Support keyboard navigation where appropriate
|
|
621
|
+
- Use semantic HTML elements
|
|
622
|
+
- Provide proper labels and error messages
|
|
623
|
+
|
|
624
|
+
### 6. Performance
|
|
625
|
+
- Use `useMemo` for expensive computations
|
|
626
|
+
- Use `useControlEffect` instead of `useEffect` for control changes
|
|
627
|
+
- Avoid unnecessary re-renders
|
|
628
|
+
- Consider virtualization for long lists
|
|
629
|
+
|
|
630
|
+
### 7. Type Safety
|
|
631
|
+
- Always define TypeScript interfaces for options
|
|
632
|
+
- Use proper type assertions with `as` operator
|
|
633
|
+
- Leverage Control<T> types from `@react-typed-forms/core`
|
|
634
|
+
|
|
635
|
+
### 8. Testing Considerations
|
|
636
|
+
- Test with disabled state
|
|
637
|
+
- Test with null/undefined values
|
|
638
|
+
- Test validation errors
|
|
639
|
+
- Test responsive behavior
|
|
640
|
+
|
|
641
|
+
## Common Patterns
|
|
642
|
+
|
|
643
|
+
### Accessing Form Data
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
// Current control value
|
|
647
|
+
const value = control.value;
|
|
648
|
+
|
|
649
|
+
// Set value
|
|
650
|
+
control.setValue("new value");
|
|
651
|
+
|
|
652
|
+
// Access parent data node
|
|
653
|
+
const parentNode = getRootDataNode(dataNode);
|
|
654
|
+
const rootControl = parentNode.control;
|
|
655
|
+
|
|
656
|
+
// Access child fields (for object controls)
|
|
657
|
+
const fields = control.fields;
|
|
658
|
+
const childValue = fields.someField.value;
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Conditional Rendering
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
const MyRenderer = createDataRenderer(
|
|
665
|
+
(props) => {
|
|
666
|
+
const { control, renderOptions } = props;
|
|
667
|
+
const options = renderOptions as MyOptions;
|
|
668
|
+
|
|
669
|
+
if (!options.enabled) {
|
|
670
|
+
return <span>{control.value}</span>; // Read-only fallback
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return <input ... />; // Full editing control
|
|
674
|
+
},
|
|
675
|
+
{ renderType: MyOptions.value }
|
|
676
|
+
);
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Handling Complex State
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
function MyComplexRenderer({ dataProps }: { dataProps: DataRendererProps }) {
|
|
683
|
+
// Use custom hooks for complex logic
|
|
684
|
+
const { state, actions } = useMyCustomLogic(dataProps.control);
|
|
685
|
+
|
|
686
|
+
// Use control effects for reactive updates
|
|
687
|
+
useControlEffect(
|
|
688
|
+
() => dataProps.control.value,
|
|
689
|
+
(newValue) => {
|
|
690
|
+
// React to value changes
|
|
691
|
+
actions.handleValueChange(newValue);
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
return <div>...</div>;
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Accessing Renderer Utilities
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
const MyRenderer = createDataRenderer(
|
|
703
|
+
(props, renderer) => {
|
|
704
|
+
// Access HTML elements
|
|
705
|
+
const { Div, Span } = renderer.html;
|
|
706
|
+
|
|
707
|
+
// Render labels
|
|
708
|
+
const labelElement = renderer.renderLabelText("My Label");
|
|
709
|
+
|
|
710
|
+
// Render children
|
|
711
|
+
const children = renderer.renderChildren(...);
|
|
712
|
+
|
|
713
|
+
return <Div>...</Div>;
|
|
714
|
+
},
|
|
715
|
+
{ renderType: MyOptions.value }
|
|
716
|
+
);
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
## Debugging Tips
|
|
720
|
+
|
|
721
|
+
1. **Renderer not showing up:**
|
|
722
|
+
- Check that `renderType` matches the `value` in CustomRenderOptions
|
|
723
|
+
- Verify the renderer is registered in `createStdRenderer`
|
|
724
|
+
- Ensure the extension is added to `EditorExtension`
|
|
725
|
+
|
|
726
|
+
2. **Configuration not working:**
|
|
727
|
+
- Check the interface matches the `buildSchema` definition
|
|
728
|
+
- Verify the type assertion in the renderer: `renderOptions as MyOptions`
|
|
729
|
+
- Check for typos in field names
|
|
730
|
+
|
|
731
|
+
3. **Control value not updating:**
|
|
732
|
+
- Use `control.setValue()` not direct assignment
|
|
733
|
+
- Check if control is disabled
|
|
734
|
+
- Verify the control is not being recreated
|
|
735
|
+
|
|
736
|
+
4. **Styling issues:**
|
|
737
|
+
- Use `rendererClass()` to merge classes
|
|
738
|
+
- Check parent container styles
|
|
739
|
+
- Verify Tailwind classes are valid
|
|
740
|
+
|
|
741
|
+
## References
|
|
742
|
+
|
|
743
|
+
### Key Imports
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
// Core schema functions
|
|
747
|
+
import {
|
|
748
|
+
createDataRenderer,
|
|
749
|
+
createGroupRenderer,
|
|
750
|
+
createAdornmentRenderer,
|
|
751
|
+
createLabelRenderer,
|
|
752
|
+
createDisplayRenderer,
|
|
753
|
+
createFormRenderer,
|
|
754
|
+
buildSchema,
|
|
755
|
+
CustomRenderOptions,
|
|
756
|
+
ControlDefinitionExtension,
|
|
757
|
+
} from "@react-typed-forms/schemas";
|
|
758
|
+
|
|
759
|
+
// Control management
|
|
760
|
+
import { Control, useControlEffect } from "@react-typed-forms/core";
|
|
761
|
+
|
|
762
|
+
// Field types
|
|
763
|
+
import {
|
|
764
|
+
boolField,
|
|
765
|
+
stringField,
|
|
766
|
+
intField,
|
|
767
|
+
stringOptionsField,
|
|
768
|
+
} from "@react-typed-forms/schemas";
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### File Locations
|
|
772
|
+
|
|
773
|
+
- **Extension Definitions**: `ServiceTasAPI/NewClientApp/client-common/formExtensions.ts`
|
|
774
|
+
- **Main Renderer**: `ServiceTasAPI/NewClientApp/client-common/renderer.tsx`
|
|
775
|
+
- **Custom Renderers**: `ServiceTasAPI/NewClientApp/client-common/renderer/`
|
|
776
|
+
|
|
777
|
+
### Related Documentation
|
|
778
|
+
|
|
779
|
+
- `@react-typed-forms/schemas` - Core form schema library
|
|
780
|
+
- `@react-typed-forms/core` - Form control management
|
|
781
|
+
- `@astroapps/schemas-*` - Pre-built renderer packages
|