Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
DEFAULT_ID_SEPARATOR,
DEFAULT_ID_PREFIX,
GlobalFormOptions,
NameGeneratorFunction,
} from '@rjsf/utils';
import _cloneDeep from 'lodash/cloneDeep';
import _forEach from 'lodash/forEach';
Expand Down Expand Up @@ -198,6 +199,7 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
* to put the second parameter before the first in its translation.
*/
translateString?: Registry['translateString'];
nameGenerator?: NameGeneratorFunction;
/** Optional configuration object with flags, if provided, allows users to override default form state behavior
* Currently only affecting minItems on array fields and handling of setting defaults based on the value of
* `emptyObjectFields`
Expand Down Expand Up @@ -985,10 +987,16 @@ export default class Form<
experimental_componentUpdateStrategy,
idSeparator = DEFAULT_ID_SEPARATOR,
idPrefix = DEFAULT_ID_PREFIX,
nameGenerator,
} = props;
const rootFieldId = uiSchema['ui:rootFieldId'];
// Omit any options that are undefined or null
return { idPrefix: rootFieldId || idPrefix, idSeparator, experimental_componentUpdateStrategy };
return {
idPrefix: rootFieldId || idPrefix,
idSeparator,
...(experimental_componentUpdateStrategy !== undefined && { experimental_componentUpdateStrategy }),
...(nameGenerator !== undefined && { nameGenerator }),
};
}

/** Returns the registry for the form */
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/components/fields/ArrayField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
formContext={formContext}
autofocus={autofocus}
rawErrors={rawErrors}
htmlName={fieldPathId.name}
/>
);
}
Expand Down Expand Up @@ -668,6 +669,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
formContext={formContext}
autofocus={autofocus}
rawErrors={rawErrors}
htmlName={fieldPathId.name}
/>
);
}
Expand Down Expand Up @@ -716,6 +718,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
rawErrors={rawErrors}
label={label}
hideLabel={!displayLabel}
htmlName={fieldPathId.name}
/>
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/fields/BooleanField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ function BooleanField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
formContext={formContext}
autofocus={autofocus}
rawErrors={rawErrors}
htmlName={fieldPathId.name}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export default function LayoutMultiSchemaField<
onFocus={onFocus}
value={selectedOption}
options={widgetOptions}
htmlName={fieldPathId.name}
/>
</FieldTemplate>
);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/fields/StringField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function StringField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
registry={registry}
placeholder={placeholder}
rawErrors={rawErrors}
htmlName={fieldPathId.name}
/>
);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/templates/BaseInputTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function BaseInputTemplate<
const {
id,
name, // remove this from ...rest
htmlName,
value,
readonly,
disabled,
Expand Down Expand Up @@ -78,7 +79,7 @@ export default function BaseInputTemplate<
<>
<input
id={id}
name={id}
name={htmlName || id}
className='form-control'
readOnly={readonly}
disabled={disabled}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/widgets/CheckboxWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function CheckboxWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F exte
onFocus,
onChange,
registry,
htmlName,
}: WidgetProps<T, S, F>) {
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate', T, S, F>(
'DescriptionFieldTemplate',
Expand Down Expand Up @@ -73,7 +74,7 @@ function CheckboxWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F exte
<input
type='checkbox'
id={id}
name={id}
name={htmlName || id}
checked={typeof value === 'undefined' ? false : value}
required={required}
disabled={disabled || readonly}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/widgets/CheckboxesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function CheckboxesWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F ex
onChange,
onBlur,
onFocus,
htmlName,
}: WidgetProps<T, S, F>) {
const checkboxesValues = Array.isArray(value) ? value : [value];

Expand Down Expand Up @@ -62,7 +63,7 @@ function CheckboxesWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F ex
<input
type='checkbox'
id={optionId(id, index)}
name={id}
name={htmlName || id}
checked={checked}
value={String(index)}
disabled={disabled || itemDisabled || readonly}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/widgets/HiddenWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps } from '@rjs
function HiddenWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>({
id,
value,
htmlName,
}: WidgetProps<T, S, F>) {
return <input type='hidden' id={id} name={id} value={typeof value === 'undefined' ? '' : value} />;
return <input type='hidden' id={id} name={htmlName || id} value={typeof value === 'undefined' ? '' : value} />;
}

export default HiddenWidget;
3 changes: 2 additions & 1 deletion packages/core/src/components/widgets/RadioWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function RadioWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
onFocus,
onChange,
id,
htmlName,
}: WidgetProps<T, S, F>) {
const { enumOptions, enumDisabled, inline, emptyValue } = options;

Expand Down Expand Up @@ -57,7 +58,7 @@ function RadioWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
type='radio'
id={optionId(id, i)}
checked={checked}
name={id}
name={htmlName || id}
required={required}
value={String(i)}
disabled={disabled || itemDisabled || readonly}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/widgets/RatingWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function RatingWidget<
onBlur,
schema,
options,
htmlName,
}: WidgetProps<T, S, F>) {
const { stars = 5, shape = 'star' } = options;

Expand Down Expand Up @@ -117,7 +118,7 @@ export default function RatingWidget<
<input
type='hidden'
id={id}
name={id}
name={htmlName || id}
value={value || ''}
required={required}
disabled={disabled || readonly}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/widgets/SelectWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function SelectWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
onBlur,
onFocus,
placeholder,
htmlName,
}: WidgetProps<T, S, F>) {
const { enumOptions, enumDisabled, emptyValue: optEmptyVal } = options;
const emptyValue = multiple ? [] : '';
Expand Down Expand Up @@ -72,7 +73,7 @@ function SelectWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
return (
<select
id={id}
name={id}
name={htmlName || id}
multiple={multiple}
role='combobox'
className='form-control'
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/components/widgets/TextareaWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function TextareaWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F exte
onChange,
onBlur,
onFocus,
htmlName,
}: WidgetProps<T, S, F>) {
const handleChange = useCallback(
({ target: { value } }: ChangeEvent<HTMLTextAreaElement>) => onChange(value === '' ? options.emptyValue : value),
Expand All @@ -36,7 +37,7 @@ function TextareaWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F exte
return (
<textarea
id={id}
name={id}
name={htmlName || id}
className='form-control'
value={value ? value : ''}
placeholder={placeholder}
Expand Down
2 changes: 0 additions & 2 deletions packages/core/test/SchemaField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ describe('SchemaField', () => {
globalFormOptions: {
idPrefix: DEFAULT_ID_PREFIX,
idSeparator: DEFAULT_ID_SEPARATOR,
experimental_componentUpdateStrategy: undefined,
},
});
});
Expand Down Expand Up @@ -94,7 +93,6 @@ describe('SchemaField', () => {
globalFormOptions: {
idPrefix: DEFAULT_ID_PREFIX,
idSeparator: DEFAULT_ID_SEPARATOR,
experimental_componentUpdateStrategy: undefined,
},
});
});
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import validationDataMerge from './validationDataMerge';
import withIdRefPrefix from './withIdRefPrefix';
import getOptionMatchingSimpleDiscriminator from './getOptionMatchingSimpleDiscriminator';
import getChangedFields from './getChangedFields';
import { bracketNameGenerator, dotNotationNameGenerator } from './nameGenerators';

export * from './types';
export * from './enums';
Expand Down Expand Up @@ -145,6 +146,8 @@ export {
utcToLocal,
validationDataMerge,
withIdRefPrefix,
bracketNameGenerator,
dotNotationNameGenerator,
};

export type { ComponentUpdateStrategy } from './shouldRender';
30 changes: 30 additions & 0 deletions packages/utils/src/nameGenerators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NameGeneratorFunction, FieldPathList } from './types';

/**
* Generates bracketed names
* Example: root[tasks][0][title]
*/
export const bracketNameGenerator: NameGeneratorFunction = (path: FieldPathList, idPrefix: string): string => {
if (!path || path.length === 0) {
return idPrefix;
}

return path.reduce<string>((acc, pathUnit, index) => {
if (index === 0) {
return `${idPrefix}[${String(pathUnit)}]`;
}
return `${acc}[${String(pathUnit)}]`;
}, '');
};

/**
* Generates dot-notation names
* Example: root.tasks.0.title
*/
export const dotNotationNameGenerator: NameGeneratorFunction = (path: FieldPathList, idPrefix: string): string => {
if (!path || path.length === 0) {
return idPrefix;
}

return `${idPrefix}.${path.map(String).join('.')}`;
};
13 changes: 10 additions & 3 deletions packages/utils/src/toFieldPathId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { FieldPathId, FieldPathList, GlobalFormOptions } from './types';
/** Constructs the `FieldPathId` for `fieldPath`. If `parentPathId` is provided, the `fieldPath` is appended to the end
* of the parent path. Then the `ID_KEY` of the resulting `FieldPathId` is constructed from the `idPrefix` and
* `idSeparator` contained within the `globalFormOptions`. If `fieldPath` is passed as an empty string, it will simply
* generate the path from the `parentPath` (if provided) and the `idPrefix` and `idSeparator`
* generate the path from the `parentPath` (if provided) and the `idPrefix` and `idSeparator`. If a `nameGenerator`
* is provided in `globalFormOptions`, it will also generate the HTML `name` attribute.
*
* @param fieldPath - The property name or array index of the current field element
* @param globalFormOptions - The `GlobalFormOptions` used to get the `idPrefix` and `idSeparator`
* @param [parentPath] - The optional `FieldPathId` or `FieldPathList` of the parent element for this field element
* @returns - The `FieldPathId` for the given `fieldPath` and the optional `parentPathId`
*/
export default function toFieldPathId(
fieldPath: string | number,
Expand All @@ -20,5 +20,12 @@ export default function toFieldPathId(
const childPath = fieldPath === '' ? [] : [fieldPath];
const path = basePath ? basePath.concat(...childPath) : childPath;
const id = [globalFormOptions.idPrefix, ...path].join(globalFormOptions.idSeparator);
return { path, [ID_KEY]: id };

// Generate name attribute if nameGenerator is provided
let name: string | undefined;
if (globalFormOptions.nameGenerator && path.length > 0) {
name = globalFormOptions.nameGenerator(path, globalFormOptions.idPrefix);
}

return { path, [ID_KEY]: id, ...(name !== undefined && { name }) };
}
12 changes: 12 additions & 0 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export type FormContextType = GenericObjectType;
*/
export type TestIdShape = Record<string, string>;

/** Function to generate HTML name attributes from path segments */
export type NameGeneratorFunction = (path: FieldPathList, idPrefix: string) => string;

/** Experimental feature that specifies the Array `minItems` default form state behavior
*/
export type Experimental_ArrayMinItems = {
Expand Down Expand Up @@ -175,6 +178,8 @@ export type FieldPathId = {
$id: string;
/** The path for a field */
path: FieldPathList;
/** The optional HTML name attribute for a field, generated by nameGenerator if provided */
name?: string;
};

/** Type describing a name used for a field in the `PathSchema` */
Expand Down Expand Up @@ -414,6 +419,11 @@ export type GlobalFormOptions = {
readonly idSeparator: string;
/** The component update strategy used by the Form and its fields for performance optimization */
readonly experimental_componentUpdateStrategy?: 'customDeep' | 'shallow' | 'always';
/** Optional function to generate custom HTML name attributes for form elements. Receives the field path segments
* and element type (object or array), and returns a custom name string. This allows backends like PHP/Rails
* (`root[tasks][0][title]`) or Django (`root__tasks-0__title`) to receive form data in their expected format.
*/
readonly nameGenerator?: NameGeneratorFunction;
};

/** The object containing the registered core, theme and custom fields and widgets as well as the root schema, form
Expand Down Expand Up @@ -850,6 +860,8 @@ export interface WidgetProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F
multiple?: boolean;
/** An array of strings listing all generated error messages from encountered errors for this widget */
rawErrors?: string[];
/** The optional custom HTML name attribute generated by the nameGenerator function, if provided */
htmlName?: string;
}

/** The definition of a React-based Widget component */
Expand Down
Loading
Loading