Skip to content

Commit 943b7d0

Browse files
committed
feat: Add support for user nameGenerator function
Closes #4693
1 parent ca2ddc2 commit 943b7d0

File tree

15 files changed

+752
-23
lines changed

15 files changed

+752
-23
lines changed

packages/core/src/components/Form.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
isObject,
1515
mergeObjects,
1616
NAME_KEY,
17+
NameGeneratorFunction,
1718
PathSchema,
1819
StrictRJSFSchema,
1920
Registry,
@@ -212,6 +213,16 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
212213
*/
213214

214215
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
216+
/** Optional function to generate HTML name attributes from path segments
217+
* @param segments - Array of path segments representing the field hierarchy
218+
* @param rootName - Optional root name prefix
219+
* @returns - The generated name attribute string, or undefined to use default behavior
220+
*/
221+
nameGenerator?: NameGeneratorFunction;
222+
/** Optional root name to use with nameGenerator
223+
* @default "root"
224+
*/
225+
rootName?: string;
215226
// Private
216227
/**
217228
* _internalFormWrapper is currently used by the semantic-ui theme to provide a custom wrapper around `<Form />`
@@ -960,6 +971,7 @@ export default class Form<
960971
} = this.props;
961972
const { schema, schemaUtils } = this.state;
962973
const { fields, templates, widgets, formContext, translateString } = getDefaultRegistry<T, S, F>();
974+
963975
return {
964976
fields: { ...fields, ...this.props.fields },
965977
templates: {
@@ -972,7 +984,13 @@ export default class Form<
972984
},
973985
widgets: { ...widgets, ...this.props.widgets },
974986
rootSchema: schema,
975-
formContext: this.props.formContext || formContext,
987+
formContext: {
988+
...(this.props.formContext || formContext),
989+
...(this.props.nameGenerator && {
990+
nameGenerator: this.props.nameGenerator,
991+
rootName: this.props.rootName || 'root',
992+
}),
993+
},
976994
schemaUtils,
977995
translateString: customTranslateString || translateString,
978996
globalUiOptions: uiSchema[UI_GLOBAL_OPTIONS_KEY],
@@ -1158,6 +1176,14 @@ export default class Form<
11581176
uiSchema={uiSchema}
11591177
errorSchema={errorSchema}
11601178
idSchema={idSchema}
1179+
pathSchema={(() => {
1180+
try {
1181+
return isObject(schema) ? registry.schemaUtils.toPathSchema(schema, '', formData) : undefined;
1182+
} catch {
1183+
// Silently handle malformed schemas in tests
1184+
return undefined;
1185+
}
1186+
})()}
11611187
idPrefix={idPrefix}
11621188
idSeparator={idSeparator}
11631189
formData={formData}

packages/core/src/components/fields/ArrayField.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
FieldProps,
1313
FormContextType,
1414
IdSchema,
15+
PathSchema,
16+
PathSegment,
1517
RJSFSchema,
1618
StrictRJSFSchema,
1719
TranslatableString,
@@ -502,6 +504,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
502504
uiSchema = {},
503505
errorSchema,
504506
idSchema,
507+
pathSchema,
505508
name,
506509
title,
507510
disabled = false,
@@ -534,6 +537,14 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
534537
const itemIdPrefix = idSchema.$id + idSeparator + index;
535538
const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);
536539

540+
// Create path schema for array item with segments
541+
const itemPathSegments: PathSegment[] = [...(pathSchema?.$segments || []), { type: 'array', key: index }];
542+
543+
const itemPathSchema: PathSchema<T> = {
544+
$name: `${pathSchema?.$name || ''}.${index}`,
545+
$segments: itemPathSegments,
546+
};
547+
537548
// Compute the item UI schema using the helper method
538549
const itemUiSchema = this.computeItemUiSchema(uiSchema, item, index, formContext);
539550

@@ -547,6 +558,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
547558
canMoveDown: index < formData.length - 1,
548559
itemSchema,
549560
itemIdSchema,
561+
itemPathSchema,
550562
itemErrorSchema,
551563
itemData: itemCast,
552564
itemUiSchema,
@@ -738,6 +750,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
738750
idPrefix,
739751
idSeparator = '_',
740752
idSchema,
753+
pathSchema,
741754
name,
742755
title,
743756
disabled = false,
@@ -787,6 +800,15 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
787800
: itemSchemas[index]) || {};
788801
const itemIdPrefix = idSchema.$id + idSeparator + index;
789802
const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);
803+
804+
// Create path schema for array item with segments
805+
const itemPathSegments: PathSegment[] = [...(pathSchema?.$segments || []), { type: 'array', key: index }];
806+
807+
const itemPathSchema: PathSchema<T> = {
808+
$name: `${pathSchema?.$name || ''}.${index}`,
809+
$segments: itemPathSegments,
810+
};
811+
790812
// Compute the item UI schema - handle both static and dynamic cases
791813
let itemUiSchema: UiSchema<T[], S, F> | undefined;
792814
if (additional) {
@@ -816,6 +838,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
816838
itemData: itemCast,
817839
itemUiSchema,
818840
itemIdSchema,
841+
itemPathSchema,
819842
itemErrorSchema,
820843
autofocus: autofocus && index === 0,
821844
onBlur,
@@ -857,6 +880,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
857880
itemData: T[];
858881
itemUiSchema: UiSchema<T[], S, F> | undefined;
859882
itemIdSchema: IdSchema<T[]>;
883+
itemPathSchema?: PathSchema<T>;
860884
itemErrorSchema?: ErrorSchema<T[]>;
861885
autofocus?: boolean;
862886
onBlur: FieldProps<T[], S, F>['onBlur'];
@@ -876,6 +900,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
876900
itemData,
877901
itemUiSchema,
878902
itemIdSchema,
903+
itemPathSchema,
879904
itemErrorSchema,
880905
autofocus,
881906
onBlur,
@@ -914,6 +939,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
914939
idPrefix={idPrefix}
915940
idSeparator={idSeparator}
916941
idSchema={itemIdSchema}
942+
pathSchema={itemPathSchema}
917943
required={this.isItemRequired(itemSchema)}
918944
onChange={this.onChangeForIndex(index)}
919945
onBlur={onBlur}

packages/core/src/components/fields/ObjectField.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
FormContextType,
99
GenericObjectType,
1010
IdSchema,
11+
PathSchema,
12+
PathSegment,
1113
RJSFSchema,
1214
StrictRJSFSchema,
1315
TranslatableString,
@@ -232,6 +234,7 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
232234
formData,
233235
errorSchema,
234236
idSchema,
237+
pathSchema,
235238
name,
236239
required = false,
237240
disabled,
@@ -282,6 +285,14 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
282285
const hidden = getUiOptions<T, S, F>(fieldUiSchema).widget === 'hidden';
283286
const fieldIdSchema: IdSchema<T> = get(idSchema, [name], {});
284287

288+
// Create path schema for property with segments
289+
const propertyPathSegments: PathSegment[] = [...(pathSchema?.$segments || []), { type: 'object', key: name }];
290+
291+
const propertyPathSchema: PathSchema<T> = (pathSchema?.[name] as PathSchema<T>) || {
292+
$name: `${pathSchema?.$name || ''}.${name}`,
293+
$segments: propertyPathSegments,
294+
};
295+
285296
return {
286297
content: (
287298
<SchemaField
@@ -292,6 +303,7 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
292303
uiSchema={fieldUiSchema}
293304
errorSchema={get(errorSchema, name)}
294305
idSchema={fieldIdSchema}
306+
pathSchema={propertyPathSchema}
295307
idPrefix={idPrefix}
296308
idSeparator={idSeparator}
297309
formData={get(formData, name)}

packages/core/src/components/fields/StringField.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function StringField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
2323
name,
2424
uiSchema,
2525
idSchema,
26+
pathSchema,
2627
formData,
2728
required,
2829
disabled = false,
@@ -37,6 +38,14 @@ function StringField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
3738
} = props;
3839
const { title, format } = schema;
3940
const { widgets, formContext, schemaUtils, globalUiOptions } = registry;
41+
42+
// Generate HTML name if nameGenerator is provided
43+
const { nameGenerator, rootName } = formContext || {};
44+
let htmlName: string | undefined;
45+
46+
if (nameGenerator && pathSchema?.$segments) {
47+
htmlName = nameGenerator(pathSchema.$segments, rootName);
48+
}
4049
const enumOptions = schemaUtils.isSelect(schema) ? optionsList<T, S, F>(schema, uiSchema) : undefined;
4150
let defaultWidget = enumOptions ? 'select' : 'text';
4251
if (format && hasWidget<T, S, F>(schema, format, widgets)) {
@@ -60,6 +69,8 @@ function StringField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
6069
uiSchema={uiSchema}
6170
id={idSchema.$id}
6271
name={name}
72+
htmlName={htmlName}
73+
pathSegments={pathSchema?.$segments}
6374
label={label}
6475
hideLabel={!displayLabel}
6576
hideError={hideError}

packages/core/src/components/templates/BaseInputTemplate.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default function BaseInputTemplate<
2323
const {
2424
id,
2525
name, // remove this from ...rest
26+
htmlName, // remove this from ...rest
2627
value,
2728
readonly,
2829
disabled,
@@ -40,11 +41,12 @@ export default function BaseInputTemplate<
4041
type,
4142
hideLabel, // remove this from ...rest
4243
hideError, // remove this from ...rest
44+
pathSegments, // remove this from ...rest
4345
...rest
4446
} = props;
4547

4648
// Note: since React 15.2.0 we can't forward unknown element attributes, so we
47-
// exclude the "options" and "schema" ones here.
49+
// exclude the "options", "schema", and other non-HTML props here.
4850
if (!id) {
4951
console.log('No id for', props);
5052
throw new Error(`no id for props ${JSON.stringify(props)}`);
@@ -78,7 +80,7 @@ export default function BaseInputTemplate<
7880
<>
7981
<input
8082
id={id}
81-
name={id}
83+
name={htmlName || id}
8284
className='form-control'
8385
readOnly={readonly}
8486
disabled={disabled}

packages/playground/src/components/Header.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import CopyLink from './CopyLink';
1515
import ThemeSelector, { ThemesType } from './ThemeSelector';
1616
import SampleSelector, { SampleSelectorProps } from './SampleSelector';
1717
import ValidatorSelector from './ValidatorSelector';
18+
import NameGeneratorSelector from './NameGeneratorSelector';
1819
import SubthemeSelector, { SubthemeType } from './SubthemeSelector';
1920
import RawValidatorTest from './RawValidatorTest';
2021

@@ -254,13 +255,15 @@ type HeaderProps = {
254255
[validatorName: string]: ValidatorType<any, RJSFSchema, any>;
255256
};
256257
validator: string;
258+
nameGenerator: string | null;
257259
liveSettings: LiveSettings;
258260
playGroundFormRef: MutableRefObject<any>;
259261
onSampleSelected: SampleSelectorProps['onSelected'];
260262
onThemeSelected: (theme: string, themeObj: ThemesType) => void;
261263
setSubtheme: Dispatch<SetStateAction<string | null>>;
262264
setStylesheet: Dispatch<SetStateAction<string | null>>;
263265
setValidator: Dispatch<SetStateAction<string>>;
266+
setNameGenerator: Dispatch<SetStateAction<string | null>>;
264267
setLiveSettings: Dispatch<SetStateAction<LiveSettings>>;
265268
setShareURL: Dispatch<SetStateAction<string | null>>;
266269
};
@@ -275,12 +278,14 @@ export default function Header({
275278
subtheme,
276279
validators,
277280
validator,
281+
nameGenerator,
278282
liveSettings,
279283
playGroundFormRef,
280284
onThemeSelected,
281285
setSubtheme,
282286
setStylesheet,
283287
setValidator,
288+
setNameGenerator,
284289
setLiveSettings,
285290
setShareURL,
286291
sampleName,
@@ -301,6 +306,13 @@ export default function Header({
301306
[setValidator],
302307
);
303308

309+
const onNameGeneratorSelected = useCallback(
310+
(nameGenerator: string | null) => {
311+
setNameGenerator(nameGenerator);
312+
},
313+
[setNameGenerator],
314+
);
315+
304316
const handleSetLiveSettings = useCallback(
305317
({ formData }: IChangeEvent) => {
306318
setLiveSettings((previousLiveSettings) => ({ ...previousLiveSettings, ...formData }));
@@ -322,6 +334,7 @@ export default function Header({
322334
theme,
323335
liveSettings,
324336
validator,
337+
nameGenerator,
325338
sampleName,
326339
}),
327340
);
@@ -331,7 +344,7 @@ export default function Header({
331344
setShareURL(null);
332345
console.error(error);
333346
}
334-
}, [formData, liveSettings, schema, theme, uiSchema, validator, setShareURL, sampleName]);
347+
}, [formData, liveSettings, schema, theme, uiSchema, validator, nameGenerator, setShareURL, sampleName]);
335348

336349
return (
337350
<div className='page-header'>
@@ -370,6 +383,7 @@ export default function Header({
370383
<SubthemeSelector subthemes={themes[theme].subthemes!} subtheme={subtheme} select={onSubthemeSelected} />
371384
)}
372385
<ValidatorSelector validators={validators} validator={validator} select={onValidatorSelected} />
386+
<NameGeneratorSelector nameGenerator={nameGenerator} select={onNameGeneratorSelected} />
373387
<HeaderButtons playGroundFormRef={playGroundFormRef} />
374388
<div style={{ marginTop: '5px' }} />
375389
<CopyLink shareURL={shareURL} onShare={onShare} />
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useCallback } from 'react';
2+
import Form, { IChangeEvent } from '@rjsf/core';
3+
import { RJSFSchema, UiSchema } from '@rjsf/utils';
4+
import localValidator from '@rjsf/validator-ajv8';
5+
6+
interface NameGeneratorSelectorProps {
7+
nameGenerator: string | null;
8+
select: (nameGenerator: string | null) => void;
9+
}
10+
11+
export default function NameGeneratorSelector({ nameGenerator, select }: NameGeneratorSelectorProps) {
12+
const schema: RJSFSchema = {
13+
type: 'string',
14+
title: 'Name Generator',
15+
oneOf: [
16+
{ const: '', title: 'None' },
17+
{ const: 'bracket', title: 'Bracket (root[field][0])' },
18+
{ const: 'dotnotation', title: 'Dot Notation (root.field.0)' },
19+
],
20+
};
21+
22+
const uiSchema: UiSchema = {
23+
'ui:placeholder': 'Select name generator',
24+
};
25+
26+
const onChange = useCallback(
27+
({ formData }: IChangeEvent) => {
28+
select(formData === '' ? null : formData);
29+
},
30+
[select],
31+
);
32+
33+
return (
34+
<Form
35+
className='form_rjsf_nameGeneratorSelector'
36+
idPrefix='rjsf_nameGeneratorSelector'
37+
schema={schema}
38+
uiSchema={uiSchema}
39+
formData={nameGenerator || ''}
40+
validator={localValidator}
41+
onChange={onChange}
42+
>
43+
<div />
44+
</Form>
45+
);
46+
}

0 commit comments

Comments
 (0)