Skip to content

Commit c2ff855

Browse files
Chore: Convert ObjectField to a stateless component
The work to convert `ObjectField` to a stateless functional component with some optimizations
1 parent db40580 commit c2ff855

File tree

39 files changed

+513
-385
lines changed

39 files changed

+513
-385
lines changed

packages/antd/src/templates/FieldTemplate/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export default function FieldTemplate<
3434
hidden,
3535
id,
3636
label,
37+
onRemovePropertyClick,
38+
onKeyRenameBlur,
3739
onDropPropertyClick,
3840
onKeyChange,
3941
rawErrors,
@@ -86,6 +88,8 @@ export default function FieldTemplate<
8688
disabled={disabled}
8789
id={id}
8890
label={label}
91+
onRemovePropertyClick={onRemovePropertyClick}
92+
onKeyRenameBlur={onKeyRenameBlur}
8993
onDropPropertyClick={onDropPropertyClick}
9094
onKeyChange={onKeyChange}
9195
readonly={readonly}

packages/antd/src/templates/ObjectFieldTemplate/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default function ObjectFieldTemplate<
4040
disabled,
4141
formData,
4242
fieldPathId,
43-
onAddClick,
43+
onAddProperty,
4444
optionalDataControl,
4545
properties,
4646
readonly,
@@ -160,7 +160,7 @@ export default function ObjectFieldTemplate<
160160
id={buttonId(fieldPathId, 'add')}
161161
className='rjsf-object-property-expand'
162162
disabled={disabled || readonly}
163-
onClick={onAddClick(schema)}
163+
onClick={onAddProperty}
164164
uiSchema={uiSchema}
165165
registry={registry}
166166
/>

packages/antd/src/templates/WrapIfAdditionalTemplate/index.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { FocusEvent } from 'react';
21
import { Col, Row, Form, Input } from 'antd';
32
import {
43
ADDITIONAL_PROPERTY_FLAG,
@@ -35,8 +34,8 @@ export default function WrapIfAdditionalTemplate<
3534
disabled,
3635
id,
3736
label,
38-
onDropPropertyClick,
39-
onKeyChange,
37+
onRemovePropertyClick,
38+
onKeyRenameBlur,
4039
readonly,
4140
required,
4241
registry,
@@ -66,8 +65,6 @@ export default function WrapIfAdditionalTemplate<
6665
);
6766
}
6867

69-
const handleBlur = ({ target }: FocusEvent<HTMLInputElement>) => onKeyChange(target && target.value);
70-
7168
// The `block` prop is not part of the `IconButtonProps` defined in the template, so put it into the uiSchema instead
7269
const uiOptions = uiSchema ? uiSchema[UI_OPTIONS_KEY] : {};
7370
const buttonUiOptions = {
@@ -97,7 +94,7 @@ export default function WrapIfAdditionalTemplate<
9794
disabled={disabled || (readonlyAsDisabled && readonly)}
9895
id={`${id}-key`}
9996
name={`${id}-key`}
100-
onBlur={!readonly ? handleBlur : undefined}
97+
onBlur={!readonly ? onKeyRenameBlur : undefined}
10198
style={INPUT_STYLE}
10299
type='text'
103100
/>
@@ -112,7 +109,7 @@ export default function WrapIfAdditionalTemplate<
112109
id={buttonId(id, 'remove')}
113110
className='rjsf-object-property-remove'
114111
disabled={disabled || readonly}
115-
onClick={onDropPropertyClick(label)}
112+
onClick={onRemovePropertyClick}
116113
uiSchema={buttonUiOptions}
117114
registry={registry}
118115
/>

packages/chakra-ui/src/FieldTemplate/FieldTemplate.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export default function FieldTemplate<
2222
displayLabel,
2323
hidden,
2424
label,
25+
onRemovePropertyClick,
26+
onKeyRenameBlur,
2527
onDropPropertyClick,
2628
onKeyChange,
2729
readonly,
@@ -53,6 +55,8 @@ export default function FieldTemplate<
5355
disabled={disabled}
5456
id={id}
5557
label={label}
58+
onRemovePropertyClick={onRemovePropertyClick}
59+
onKeyRenameBlur={onKeyRenameBlur}
5660
onDropPropertyClick={onDropPropertyClick}
5761
onKeyChange={onKeyChange}
5862
readonly={readonly}

packages/chakra-ui/src/ObjectFieldTemplate/ObjectFieldTemplate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default function ObjectFieldTemplate<
2929
schema,
3030
formData,
3131
optionalDataControl,
32-
onAddClick,
32+
onAddProperty,
3333
registry,
3434
} = props;
3535
const uiOptions = getUiOptions<T, S, F>(uiSchema);
@@ -81,7 +81,7 @@ export default function ObjectFieldTemplate<
8181
<AddButton
8282
id={buttonId(fieldPathId, 'add')}
8383
className='rjsf-object-property-expand'
84-
onClick={onAddClick(schema)}
84+
onClick={onAddProperty}
8585
disabled={disabled || readonly}
8686
uiSchema={uiSchema}
8787
registry={registry}

packages/chakra-ui/src/WrapIfAdditionalTemplate/WrapIfAdditionalTemplate.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { FocusEvent } from 'react';
21
import {
32
ADDITIONAL_PROPERTY_FLAG,
43
buttonId,
@@ -24,8 +23,8 @@ export default function WrapIfAdditionalTemplate<
2423
disabled,
2524
id,
2625
label,
27-
onDropPropertyClick,
28-
onKeyChange,
26+
onRemovePropertyClick,
27+
onKeyRenameBlur,
2928
readonly,
3029
registry,
3130
required,
@@ -45,8 +44,6 @@ export default function WrapIfAdditionalTemplate<
4544
);
4645
}
4746

48-
const handleBlur = ({ target }: FocusEvent<HTMLInputElement>) => onKeyChange(target.value);
49-
5047
return (
5148
<Grid key={`${id}-key`} className={classNames} style={style} alignItems='center' gap={2}>
5249
<GridItem>
@@ -56,7 +53,7 @@ export default function WrapIfAdditionalTemplate<
5653
disabled={disabled || readonly}
5754
id={`${id}-key`}
5855
name={`${id}-key`}
59-
onBlur={!readonly ? handleBlur : undefined}
56+
onBlur={!readonly ? onKeyRenameBlur : undefined}
6057
type='text'
6158
mb={1}
6259
/>
@@ -68,7 +65,7 @@ export default function WrapIfAdditionalTemplate<
6865
id={buttonId(id, 'remove')}
6966
className='rjsf-object-property-remove'
7067
disabled={disabled || readonly}
71-
onClick={onDropPropertyClick(label)}
68+
onClick={onRemovePropertyClick}
7269
uiSchema={uiSchema}
7370
registry={registry}
7471
/>

packages/core/src/components/Form.tsx

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
DEFAULT_ID_PREFIX,
4343
GlobalFormOptions,
4444
ERRORS_KEY,
45+
ID_KEY,
4546
} from '@rjsf/utils';
4647
import _cloneDeep from 'lodash/cloneDeep';
4748
import _get from 'lodash/get';
@@ -269,24 +270,38 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
269270
retrievedSchema: S;
270271
/** Flag indicating whether the initial form defaults have been generated */
271272
initialDefaultsGenerated: boolean;
273+
/** The registry (re)computed only when props changed */
274+
registry: Registry<T, S, F>;
272275
}
273276

274277
/** The event data passed when changes have been made to the form, includes everything from the `FormState` except
275278
* the schema validation errors. An additional `status` is added when returned from `onSubmit`
276279
*/
277280
export interface IChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
278-
extends Omit<
281+
extends Pick<
279282
FormState<T, S, F>,
280-
| 'schemaValidationErrors'
281-
| 'schemaValidationErrorSchema'
282-
| 'retrievedSchema'
283-
| 'customErrors'
284-
| 'initialDefaultsGenerated'
283+
'schema' | 'uiSchema' | 'fieldPathId' | 'schemaUtils' | 'formData' | 'edit' | 'errors' | 'errorSchema'
285284
> {
286285
/** The status of the form when submitted */
287286
status?: 'submitted';
288287
}
289288

289+
/** Converts the full `FormState` into the `IChangeEvent` version by picking out the public values
290+
*
291+
* @param state - The state of the form
292+
* @param status - The status provided by the onSubmit
293+
* @returns - The `IChangeEvent` for the state
294+
*/
295+
function toIChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
296+
state: FormState<T, S, F>,
297+
status?: IChangeEvent['status'],
298+
): IChangeEvent<T, S, F> {
299+
return {
300+
..._pick(state, ['schema', 'uiSchema', 'fieldPathId', 'schemaUtils', 'formData', 'edit', 'errors', 'errorSchema']),
301+
...(status !== undefined ? { status } : undefined),
302+
};
303+
}
304+
290305
/** The definition of a pending change that will be processed in the `onChange` handler
291306
*/
292307
interface PendingChange<T> {
@@ -330,7 +345,7 @@ export default class Form<
330345

331346
this.state = this.getStateFromProps(props, props.formData);
332347
if (this.props.onChange && !deepEquals(this.state.formData, this.props.formData)) {
333-
this.props.onChange(this.state);
348+
this.props.onChange(toIChangeEvent(this.state));
334349
}
335350
this.formElement = createRef();
336351
}
@@ -413,7 +428,7 @@ export default class Form<
413428
!deepEquals(nextState.formData, prevState.formData) &&
414429
this.props.onChange
415430
) {
416-
this.props.onChange(nextState);
431+
this.props.onChange(toIChangeEvent(nextState));
417432
}
418433
this.setState(nextState);
419434
}
@@ -545,7 +560,14 @@ export default class Form<
545560
errorSchema = mergedErrors.errorSchema;
546561
}
547562

548-
const fieldPathId = toFieldPathId('', this.getGlobalFormOptions(this.props));
563+
// Only store a new registry when the props cause a different one to be created
564+
const newRegistry = this.getRegistry(props, rootSchema, schemaUtils);
565+
const registry = deepEquals(state.registry, newRegistry) ? state.registry : newRegistry;
566+
// Only compute a new `fieldPathId` when the `idPrefix` is different than the existing fieldPathId's ID_KEY
567+
const fieldPathId =
568+
state.fieldPathId && state.fieldPathId?.[ID_KEY] === registry.globalFormOptions.idPrefix
569+
? state.fieldPathId
570+
: toFieldPathId('', registry.globalFormOptions);
549571
const nextState: FormState<T, S, F> = {
550572
schemaUtils,
551573
schema: rootSchema,
@@ -559,6 +581,7 @@ export default class Form<
559581
schemaValidationErrorSchema,
560582
retrievedSchema: _retrievedSchema,
561583
initialDefaultsGenerated: true,
584+
registry,
562585
};
563586
return nextState;
564587
}
@@ -867,7 +890,7 @@ export default class Form<
867890
}
868891
this.setState(state as FormState<T, S, F>, () => {
869892
if (onChange) {
870-
onChange({ ...this.state, ...state }, id);
893+
onChange(toIChangeEvent({ ...this.state, ...state }), id);
871894
}
872895
// Now remove the change we just completed and call this again
873896
this.pendingChanges.shift();
@@ -909,7 +932,7 @@ export default class Form<
909932
customErrors: undefined,
910933
} as FormState<T, S, F>;
911934

912-
this.setState(state, () => onChange && onChange({ ...this.state, ...state }));
935+
this.setState(state, () => onChange && onChange(toIChangeEvent({ ...this.state, ...state })));
913936
};
914937

915938
/** Callback function to handle when a field on the form is blurred. Calls the `onBlur` callback for the `Form` if it
@@ -975,7 +998,7 @@ export default class Form<
975998
},
976999
() => {
9771000
if (onSubmit) {
978-
onSubmit({ ...this.state, formData: newFormData, status: 'submitted' }, event);
1001+
onSubmit(toIChangeEvent({ ...this.state, formData: newFormData }, 'submitted'), event);
9791002
}
9801003
},
9811004
);
@@ -1000,28 +1023,27 @@ export default class Form<
10001023
return { idPrefix: rootFieldId || idPrefix, idSeparator, experimental_componentUpdateStrategy };
10011024
}
10021025

1003-
/** Returns the registry for the form */
1004-
getRegistry(): Registry<T, S, F> {
1005-
const { translateString: customTranslateString, uiSchema = {} } = this.props;
1006-
const { schema, schemaUtils } = this.state;
1026+
/** Computed the registry for the form using the given `props`, `schema` and `schemaUtils` */
1027+
getRegistry(props: FormProps<T, S, F>, schema: S, schemaUtils: SchemaUtilsType<T, S, F>): Registry<T, S, F> {
1028+
const { translateString: customTranslateString, uiSchema = {} } = props;
10071029
const { fields, templates, widgets, formContext, translateString } = getDefaultRegistry<T, S, F>();
10081030
return {
1009-
fields: { ...fields, ...this.props.fields },
1031+
fields: { ...fields, ...props.fields },
10101032
templates: {
10111033
...templates,
1012-
...this.props.templates,
1034+
...props.templates,
10131035
ButtonTemplates: {
10141036
...templates.ButtonTemplates,
1015-
...this.props.templates?.ButtonTemplates,
1037+
...props.templates?.ButtonTemplates,
10161038
},
10171039
},
1018-
widgets: { ...widgets, ...this.props.widgets },
1040+
widgets: { ...widgets, ...props.widgets },
10191041
rootSchema: schema,
1020-
formContext: this.props.formContext || formContext,
1042+
formContext: props.formContext || formContext,
10211043
schemaUtils,
10221044
translateString: customTranslateString || translateString,
10231045
globalUiOptions: uiSchema[UI_GLOBAL_OPTIONS_KEY],
1024-
globalFormOptions: this.getGlobalFormOptions(this.props),
1046+
globalFormOptions: this.getGlobalFormOptions(props),
10251047
};
10261048
}
10271049

@@ -1162,8 +1184,7 @@ export default class Form<
11621184
_internalFormWrapper,
11631185
} = this.props;
11641186

1165-
const { schema, uiSchema, formData, errorSchema, fieldPathId } = this.state;
1166-
const registry = this.getRegistry();
1187+
const { schema, uiSchema, formData, errorSchema, fieldPathId, registry } = this.state;
11671188
const { SchemaField: _SchemaField } = registry.fields;
11681189
const { SubmitButton } = registry.templates.ButtonTemplates;
11691190
// The `semantic-ui` and `material-ui` themes have `_internalFormWrapper`s that take an `as` prop that is the

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,8 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
519519
const _schemaItems: S = isObject(schema.items) ? (schema.items as S) : ({} as S);
520520
const itemsSchema: S = schemaUtils.retrieveSchema(_schemaItems);
521521
const formData = keyedToPlainFormData(this.state.keyedFormData);
522-
const renderOptionalField = shouldRenderOptionalField(registry, schema, required, uiSchema);
523-
const hasFormData = isFormDataAvailable(this.props.formData);
522+
const renderOptionalField = shouldRenderOptionalField<T[], S, F>(registry, schema, required, uiSchema);
523+
const hasFormData = isFormDataAvailable<T[]>(this.props.formData);
524524
const canAdd = this.canAddItem(formData) && (!renderOptionalField || hasFormData);
525525
const actualFormData = hasFormData ? keyedFormData : [];
526526
const extraClass = renderOptionalField ? ' rjsf-optional-array-field' : '';
@@ -752,8 +752,8 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
752752
const uiOptions = getUiOptions<T[], S, F>(uiSchema);
753753
const { schemaUtils, fields, formContext, globalFormOptions } = registry;
754754
const { OptionalDataControlsField } = fields;
755-
const renderOptionalField = shouldRenderOptionalField(registry, schema, required, uiSchema);
756-
const hasFormData = isFormDataAvailable(formData);
755+
const renderOptionalField = shouldRenderOptionalField<T[], S, F>(registry, schema, required, uiSchema);
756+
const hasFormData = isFormDataAvailable<T[]>(formData);
757757
const _schemaItems: S[] = isObject(schema.items) ? (schema.items as S[]) : ([] as S[]);
758758
const itemSchemas = _schemaItems.map((item: S, index: number) =>
759759
schemaUtils.retrieveSchema(item, items[index] as unknown as T[]),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ export default function LayoutMultiSchemaField<
195195
displayLabel={displayLabel}
196196
errors={errors}
197197
onChange={onChange}
198+
onKeyRenameBlur={noop}
199+
onRemovePropertyClick={noop}
198200
onDropPropertyClick={ignored}
199201
onKeyChange={ignored}
200202
>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
167167
globalUiOptions,
168168
);
169169
const isOptionalRender = shouldRenderOptionalField<T, S, F>(registry, schema, required, uiSchema);
170-
const hasFormData = isFormDataAvailable(formData);
170+
const hasFormData = isFormDataAvailable<T>(formData);
171171

172172
const { selectedOption, retrievedOptions } = this.state;
173173
const {

0 commit comments

Comments
 (0)