Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ec120e4
feat(ArrayField): add dynamic uiSchema function support
chathuraa Jun 14, 2025
7070127
feat(ArrayField): implement dynamic uiSchema.items function support
chathuraa Jun 14, 2025
b064129
fix: improve error messages for dynamic uiSchema functions
chathuraa Jun 14, 2025
e21fab9
fix(ArrayField): allow undefined UI schema for array items
chathuraa Jun 14, 2025
6da9a53
fix: preserve undefined values in uiSchema handling
chathuraa Jun 14, 2025
4c45993
fix: add id to Fieldset component in ArrayFieldTemplate
chathuraa Jun 14, 2025
0000a02
docs: enhance dynamic uiSchema documentation with examples and best p…
chathuraa Jun 21, 2025
cc646d8
Merge remote-tracking branch 'origin/main' into feat/dynamic-uischema…
chathuraa Jun 21, 2025
c880f1f
fix: replace nanoid with lodash/uniqueId and handle null uiSchema
chathuraa Jul 10, 2025
873059b
fix: correct CheckboxWidget event handlers and enhance getUiOptions
chathuraa Jul 10, 2025
80e0206
refactor(ArrayField): extract UI schema computation logic and fix che…
chathuraa Jul 11, 2025
fea1569
fix: pass checkbox state in onFocus and onBlur callbacks
chathuraa Jul 11, 2025
0c877a2
Merge branch 'main' into feat/dynamic-uischema-array-items
chathuraa Jul 11, 2025
6fd9469
Merge remote-tracking branch 'fork/feat/dynamic-uischema-array-items'…
chathuraa Jul 11, 2025
96bdcf4
Merge branch 'main' into feat/dynamic-uischema-array-items
chathuraa Jul 25, 2025
5280e5a
Merge remote-tracking branch 'origin/main' into feat/dynamic-uischema…
chathuraa Aug 2, 2025
bfe373d
fix: ensure validator passed in Form rerenders & prevent null schema …
chathuraa Aug 2, 2025
32ff5b0
fix: use current value instead of event target in checkbox onFocus/on…
chathuraa Aug 11, 2025
2ebf860
Merge branch 'main' into feat/dynamic-uischema-array-items
chathuraa Aug 11, 2025
a5760f6
Merge branch 'main' into feat/dynamic-uischema-array-items
chathuraa Aug 12, 2025
41c079b
docs: updated the docs - added support for dynamic UI schema in array…
chathuraa Aug 12, 2025
1c4ac14
Merge remote-tracking branch 'fork/feat/dynamic-uischema-array-items'…
chathuraa Aug 12, 2025
7d08762
Merge branch 'main' into feat/dynamic-uischema-array-items
chathuraa Aug 12, 2025
051a7f5
docs(migration): add documentation for dynamic UI schema for array items
chathuraa Aug 12, 2025
2826971
Merge remote-tracking branch 'fork/feat/dynamic-uischema-array-items'…
chathuraa Aug 12, 2025
8f57e97
Update CHANGELOG.md
chathuraa Aug 12, 2025
9563af5
Update packages/docs/docs/migration-guides/v6.x upgrade guide.md
chathuraa Aug 12, 2025
1300100
Update packages/docs/docs/migration-guides/v6.x upgrade guide.md
chathuraa Aug 12, 2025
d10c54c
Update packages/docs/docs/json-schema/arrays.md
chathuraa Aug 12, 2025
b8a385a
Update packages/docs/docs/api-reference/dynamic-ui-schema-examples.md
chathuraa Aug 12, 2025
3a4bc80
Update packages/docs/docs/api-reference/dynamic-ui-schema-examples.md
chathuraa Aug 12, 2025
b068a2c
Update packages/docs/docs/api-reference/uiSchema.md
chathuraa Aug 12, 2025
4e96ecc
Update packages/docs/docs/api-reference/dynamic-ui-schema-examples.md
chathuraa Aug 12, 2025
0d4a931
docs: added about correcting checkbox widget focus handlers across al…
chathuraa Aug 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions DYNAMIC_UISCHEMA_EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# Dynamic uiSchema Examples

## Backward Compatibility Examples

### Example 1: Traditional Static uiSchema (No Changes Required)

```javascript
// This continues to work exactly as before
const uiSchema = {
guests: {
items: {
name: { 'ui:placeholder': 'Enter guest name' },
age: { 'ui:widget': 'updown' },
relationship: { 'ui:widget': 'select' }
}
}
};
```

### Example 2: Dynamic uiSchema with Function

```javascript
// New functionality - dynamic UI based on item data
const uiSchema = {
guests: {
items: (itemData, index, formContext) => {
// Note: For newly added items, `itemData` will be undefined or contain default values.
// Using optional chaining (`?.`) is recommended to handle this case gracefully.

// Base UI schema for all items
const baseUiSchema = {
name: { 'ui:placeholder': `Guest ${index + 1} name` },
relationship: { 'ui:widget': 'select' }
};

// Conditionally modify UI based on data
if (itemData?.relationship === 'child') {
return {
...baseUiSchema,
age: {
'ui:widget': 'updown',
'ui:help': 'Age is required for children',
'ui:options': { min: 0, max: 17 }
},
guardianName: {
'ui:placeholder': 'Parent/Guardian name'
},
mealPreference: { 'ui:widget': 'hidden' }
};
}

if (itemData?.relationship === 'adult') {
return {
...baseUiSchema,
age: { 'ui:widget': 'hidden' },
guardianName: { 'ui:widget': 'hidden' },
mealPreference: {
'ui:widget': 'select',
'ui:placeholder': 'Select meal preference'
}
};
}

// Default for new items or unknown relationships
return baseUiSchema;
}
}
};
```
### Example 3: Using Form Context
```javascript
const uiSchema = {
participants: {
items: (itemData, index, formContext) => {
// Access form-wide settings
const isConference = formContext?.eventType === 'conference';

return {
name: { 'ui:placeholder': 'Participant name' },
email: { 'ui:widget': 'email' },
// Show workshop selection only for conference events
workshop: isConference
? { 'ui:widget': 'select' }
: { 'ui:widget': 'hidden' }
};
}
}
};
```
### Example 4: Falsy Return Values
```javascript
const uiSchema = {
items: {
items: (itemData, index) => {
// Only apply custom UI to specific items
if (itemData?.needsCustomUI) {
return {
field1: { 'ui:widget': 'textarea' },
field2: { 'ui:help': 'This item needs special attention' }
};
}

// Return null or undefined to use default UI rendering
// This is useful for conditionally applying custom UI
return null;
}
}
};
```
### Example 5: Dynamic UI for Fixed Arrays
For fixed/tuple arrays (where schema.items is an array), the dynamic function can be applied to each position:
```javascript
const schema = {
type: 'array',
items: [
{ type: 'string', title: 'First Name' },
{ type: 'string', title: 'Last Name' },
{ type: 'object', title: 'Details', properties: { age: { type: 'number' }, role: { type: 'string' } } }
]
};

const uiSchema = {
items: [
{ 'ui:placeholder': 'Enter first name' }, // Static UI for first item
{ 'ui:placeholder': 'Enter last name' }, // Static UI for second item
// Dynamic UI for third item based on its data
(itemData, index) => {
if (itemData?.role === 'admin') {
return {
age: { 'ui:widget': 'hidden' },
role: { 'ui:help': 'Admin role selected' }
};
}
return {
age: { 'ui:widget': 'updown' },
role: { 'ui:widget': 'select' }
};
}
]
};
```
## Schema Example
```javascript
const schema = {
type: 'object',
properties: {
guests: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', title: 'Name' },
age: { type: 'number', title: 'Age' },
relationship: {
type: 'string',
title: 'Relationship',
enum: ['adult', 'child', 'senior']
},
guardianName: { type: 'string', title: 'Guardian Name' },
mealPreference: {
type: 'string',
title: 'Meal Preference',
enum: ['vegetarian', 'vegan', 'standard', 'gluten-free']
}
},
required: ['name', 'relationship']
}
}
}
};
```
## Key Benefits
1. **Backward Compatible**: Existing forms with object-based `uiSchema.items` continue to work without any changes
2. **Progressive Enhancement**: Developers can opt-in to dynamic behavior when needed
3. **Flexible**: Access to item data, index, and form context enables complex UI logic
4. **Safe**: Built-in error handling prevents the entire form from crashing if your function throws an error. When an error occurs for a specific item, it will be caught and logged to the developer console, and the UI for that item will fall back to the default rendering. This ensures the rest of the form remains functional while making debugging easier.
5. **On-Demand Execution**: The function is executed on-demand during the render cycle. However, as it runs for each array item, performance should be carefully managed for large lists (see Performance Considerations below).
## Key Behaviors
- **Falsy Returns**: If your function returns a falsy value (e.g., `null` or `undefined`), the UI for that specific item will fall back to its default rendering. This allows you to conditionally apply custom UI only when needed.
- **Error Handling**: If your function throws an error, it will be caught and logged to the console. The form will continue to work, using default UI for the affected item.
- **New Items**: When a new item is added to the array, `itemData` will be `undefined` or contain default values from the schema. Always use optional chaining (`?.`) to safely access properties.
## Performance Considerations
When using dynamic `uiSchema.items` functions, keep in mind:
- The function is executed **on every render** for **each array item**
- For large arrays, this can impact performance if the function performs expensive operations
- Best practices:
- Keep the function logic lightweight and fast
- Avoid heavy computations or external API calls within the function
- Consider memoizing results if the same inputs produce the same outputs
- For complex logic, pre-compute values and store them in formContext or component state
Example of a performance-optimized approach:
```javascript
// In your React component that renders the form:
const MyFormComponent = ({ schema, formData }) => {
// Pre-compute expensive data once, and only re-compute if dependencies change
const expensiveData = useMemo(() => computeExpensiveData(), [/* dependencies */]);

// Define the uiSchema inside the component so it can access the memoized data
const uiSchema = {
myArrayField: { // Target your specific array field
items: (itemData, index, formContext) => {
// Use the pre-computed data - this is very fast
const config = expensiveData[itemData?.type] || defaultConfig;

return {
field: { 'ui:widget': config.widget }
};
}
}
};

return <Form schema={schema} uiSchema={uiSchema} formData={formData} />;
};
```
4 changes: 2 additions & 2 deletions packages/chakra-ui/src/CheckboxWidget/CheckboxWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export default function CheckboxWidget<
const description = options.description || schema.description;

const _onChange = ({ checked }: CheckboxCheckedChangeDetails) => onChange(checked);
const _onBlur = ({ target }: FocusEvent<HTMLInputElement | any>) => onBlur(id, target && target.value);
const _onFocus = ({ target }: FocusEvent<HTMLInputElement | any>) => onFocus(id, target && target.value);
const _onBlur = ({ target }: FocusEvent<HTMLInputElement | any>) => onBlur(id, target && target.checked);
const _onFocus = ({ target }: FocusEvent<HTMLInputElement | any>) => onFocus(id, target && target.checked);

const chakraProps = getChakra({ uiSchema });

Expand Down
64 changes: 55 additions & 9 deletions packages/core/src/components/fields/ArrayField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import isObject from 'lodash/isObject';
import set from 'lodash/set';
import { nanoid } from 'nanoid';
import uniqueId from 'lodash/uniqueId';

/** Type used to represent the keyed form data used in the state */
type KeyedFormDataType<T> = { key: string; item: T };
Expand All @@ -37,7 +37,7 @@ type ArrayFieldState<T> = {

/** Used to generate a unique ID for an element in a row */
function generateRowId() {
return nanoid();
return uniqueId('rjsf-array-item-');
}

/** Converts the `formData` into `KeyedFormDataType` data, using the `generateRowId()` function to create the key
Expand Down Expand Up @@ -423,6 +423,39 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
onChange(value, undefined, idSchema && idSchema.$id);
};

/** Helper method to compute item UI schema for both normal and fixed arrays
* Handles both static object and dynamic function cases
*
* @param uiSchema - The parent UI schema containing items definition
* @param item - The item data
* @param index - The index of the item
* @param formContext - The form context
* @returns The computed UI schema for the item
*/
private computeItemUiSchema(
uiSchema: UiSchema<T[], S, F>,
item: T,
index: number,
formContext: F,
): UiSchema<T[], S, F> | undefined {
if (typeof uiSchema.items === 'function') {
try {
// Call the function with item data, index, and form context
// TypeScript now correctly infers the types thanks to the ArrayElement type in UiSchema
const result = uiSchema.items(item, index, formContext);
// Only use the result if it's truthy
return result as UiSchema<T[], S, F>;
} catch (e) {
console.error(`Error executing dynamic uiSchema.items function for item at index ${index}:`, e);
// Fall back to undefined to allow the field to still render
return undefined;
}
} else {
// Static object case - preserve undefined to maintain backward compatibility
return uiSchema.items as UiSchema<T[], S, F> | undefined;
}
}

/** Renders the `ArrayField` depending on the specific needs of the schema and uischema elements
*/
render() {
Expand Down Expand Up @@ -500,6 +533,10 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema<T[]>) : undefined;
const itemIdPrefix = idSchema.$id + idSeparator + index;
const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);

// Compute the item UI schema using the helper method
const itemUiSchema = this.computeItemUiSchema(uiSchema, item, index, formContext);

return this.renderArrayFieldItem({
key,
index,
Expand All @@ -512,7 +549,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
itemIdSchema,
itemErrorSchema,
itemData: itemCast,
itemUiSchema: uiSchema.items,
itemUiSchema,
autofocus: autofocus && index === 0,
onBlur,
onFocus,
Expand Down Expand Up @@ -751,11 +788,20 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
: itemSchemas[index]) || {};
const itemIdPrefix = idSchema.$id + idSeparator + index;
const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);
const itemUiSchema = additional
? uiSchema.additionalItems || {}
: Array.isArray(uiSchema.items)
? uiSchema.items[index]
: uiSchema.items || {};
// Compute the item UI schema - handle both static and dynamic cases
let itemUiSchema: UiSchema<T[], S, F> | undefined;
if (additional) {
// For additional items, use additionalItems uiSchema
itemUiSchema = uiSchema.additionalItems as UiSchema<T[], S, F>;
} else {
// For fixed items, uiSchema.items can be an array, a function, or a single object
if (Array.isArray(uiSchema.items)) {
itemUiSchema = uiSchema.items[index] as UiSchema<T[], S, F>;
} else {
// Use the helper method for function or static object cases
itemUiSchema = this.computeItemUiSchema(uiSchema, item, index, formContext);
}
}
const itemErrorSchema = errorSchema ? (errorSchema[index] as ErrorSchema<T[]>) : undefined;

return this.renderArrayFieldItem({
Expand Down Expand Up @@ -811,7 +857,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
canMoveDown: boolean;
itemSchema: S;
itemData: T[];
itemUiSchema: UiSchema<T[], S, F>;
itemUiSchema: UiSchema<T[], S, F> | undefined;
itemIdSchema: IdSchema<T[]>;
itemErrorSchema?: ErrorSchema<T[]>;
autofocus?: boolean;
Expand Down
Loading