Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions packages/antd/src/templates/FieldTemplate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export default function FieldTemplate<
hidden,
id,
label,
onRemovePropertyClick,
onKeyRenameBlur,
onDropPropertyClick,
onKeyChange,
rawErrors,
Expand Down Expand Up @@ -86,6 +88,8 @@ export default function FieldTemplate<
disabled={disabled}
id={id}
label={label}
onRemovePropertyClick={onRemovePropertyClick}
onKeyRenameBlur={onKeyRenameBlur}
onDropPropertyClick={onDropPropertyClick}
onKeyChange={onKeyChange}
readonly={readonly}
Expand Down
4 changes: 2 additions & 2 deletions packages/antd/src/templates/ObjectFieldTemplate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function ObjectFieldTemplate<
disabled,
formData,
fieldPathId,
onAddClick,
onAddProperty,
optionalDataControl,
properties,
readonly,
Expand Down Expand Up @@ -160,7 +160,7 @@ export default function ObjectFieldTemplate<
id={buttonId(fieldPathId, 'add')}
className='rjsf-object-property-expand'
disabled={disabled || readonly}
onClick={onAddClick(schema)}
onClick={onAddProperty}
uiSchema={uiSchema}
registry={registry}
/>
Expand Down
11 changes: 4 additions & 7 deletions packages/antd/src/templates/WrapIfAdditionalTemplate/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { FocusEvent } from 'react';
import { Col, Row, Form, Input } from 'antd';
import {
ADDITIONAL_PROPERTY_FLAG,
Expand Down Expand Up @@ -35,8 +34,8 @@ export default function WrapIfAdditionalTemplate<
disabled,
id,
label,
onDropPropertyClick,
onKeyChange,
onRemovePropertyClick,
onKeyRenameBlur,
readonly,
required,
registry,
Expand Down Expand Up @@ -66,8 +65,6 @@ export default function WrapIfAdditionalTemplate<
);
}

const handleBlur = ({ target }: FocusEvent<HTMLInputElement>) => onKeyChange(target && target.value);

// The `block` prop is not part of the `IconButtonProps` defined in the template, so put it into the uiSchema instead
const uiOptions = uiSchema ? uiSchema[UI_OPTIONS_KEY] : {};
const buttonUiOptions = {
Expand Down Expand Up @@ -97,7 +94,7 @@ export default function WrapIfAdditionalTemplate<
disabled={disabled || (readonlyAsDisabled && readonly)}
id={`${id}-key`}
name={`${id}-key`}
onBlur={!readonly ? handleBlur : undefined}
onBlur={!readonly ? onKeyRenameBlur : undefined}
style={INPUT_STYLE}
type='text'
/>
Expand All @@ -112,7 +109,7 @@ export default function WrapIfAdditionalTemplate<
id={buttonId(id, 'remove')}
className='rjsf-object-property-remove'
disabled={disabled || readonly}
onClick={onDropPropertyClick(label)}
onClick={onRemovePropertyClick}
uiSchema={buttonUiOptions}
registry={registry}
/>
Expand Down
4 changes: 4 additions & 0 deletions packages/chakra-ui/src/FieldTemplate/FieldTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default function FieldTemplate<
displayLabel,
hidden,
label,
onRemovePropertyClick,
onKeyRenameBlur,
onDropPropertyClick,
onKeyChange,
readonly,
Expand Down Expand Up @@ -53,6 +55,8 @@ export default function FieldTemplate<
disabled={disabled}
id={id}
label={label}
onRemovePropertyClick={onRemovePropertyClick}
onKeyRenameBlur={onKeyRenameBlur}
onDropPropertyClick={onDropPropertyClick}
onKeyChange={onKeyChange}
readonly={readonly}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function ObjectFieldTemplate<
schema,
formData,
optionalDataControl,
onAddClick,
onAddProperty,
registry,
} = props;
const uiOptions = getUiOptions<T, S, F>(uiSchema);
Expand Down Expand Up @@ -81,7 +81,7 @@ export default function ObjectFieldTemplate<
<AddButton
id={buttonId(fieldPathId, 'add')}
className='rjsf-object-property-expand'
onClick={onAddClick(schema)}
onClick={onAddProperty}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { FocusEvent } from 'react';
import {
ADDITIONAL_PROPERTY_FLAG,
buttonId,
Expand All @@ -24,8 +23,8 @@ export default function WrapIfAdditionalTemplate<
disabled,
id,
label,
onDropPropertyClick,
onKeyChange,
onRemovePropertyClick,
onKeyRenameBlur,
readonly,
registry,
required,
Expand All @@ -45,8 +44,6 @@ export default function WrapIfAdditionalTemplate<
);
}

const handleBlur = ({ target }: FocusEvent<HTMLInputElement>) => onKeyChange(target.value);

return (
<Grid key={`${id}-key`} className={classNames} style={style} alignItems='center' gap={2}>
<GridItem>
Expand All @@ -56,7 +53,7 @@ export default function WrapIfAdditionalTemplate<
disabled={disabled || readonly}
id={`${id}-key`}
name={`${id}-key`}
onBlur={!readonly ? handleBlur : undefined}
onBlur={!readonly ? onKeyRenameBlur : undefined}
type='text'
mb={1}
/>
Expand All @@ -68,7 +65,7 @@ export default function WrapIfAdditionalTemplate<
id={buttonId(id, 'remove')}
className='rjsf-object-property-remove'
disabled={disabled || readonly}
onClick={onDropPropertyClick(label)}
onClick={onRemovePropertyClick}
uiSchema={uiSchema}
registry={registry}
/>
Expand Down
87 changes: 64 additions & 23 deletions packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
DEFAULT_ID_PREFIX,
GlobalFormOptions,
ERRORS_KEY,
ID_KEY,
} from '@rjsf/utils';
import _cloneDeep from 'lodash/cloneDeep';
import _get from 'lodash/get';
Expand Down Expand Up @@ -256,37 +257,71 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
errors: RJSFValidationError[];
/** The current errors, in `ErrorSchema` format, for the form, includes `extraErrors` */
errorSchema: ErrorSchema<T>;
// Private
/** The current list of errors for the form directly from schema validation, does NOT include `extraErrors` */
schemaValidationErrors: RJSFValidationError[];
/** The current errors, in `ErrorSchema` format, for the form directly from schema validation, does NOT include
* `extraErrors`
*/
schemaValidationErrorSchema: ErrorSchema<T>;
// Private
/** A container used to handle custom errors provided via `onChange` */
customErrors?: ErrorSchemaBuilder<T>;
/** @description result of schemaUtils.retrieveSchema(schema, formData). This a memoized value to avoid re calculate at internal functions (getStateFromProps, onChange) */
retrievedSchema: S;
/** Flag indicating whether the initial form defaults have been generated */
initialDefaultsGenerated: boolean;
/** The registry (re)computed only when props changed */
registry: Registry<T, S, F>;
}

/** The event data passed when changes have been made to the form, includes everything from the `FormState` except
* the schema validation errors. An additional `status` is added when returned from `onSubmit`
*/
export interface IChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
extends Omit<
extends Pick<
FormState<T, S, F>,
| 'schema'
| 'uiSchema'
| 'fieldPathId'
| 'schemaUtils'
| 'formData'
| 'edit'
| 'errors'
| 'errorSchema'
| 'schemaValidationErrors'
| 'schemaValidationErrorSchema'
| 'retrievedSchema'
| 'customErrors'
| 'initialDefaultsGenerated'
> {
/** The status of the form when submitted */
status?: 'submitted';
}

/** Converts the full `FormState` into the `IChangeEvent` version by picking out the public values
*
* @param state - The state of the form
* @param status - The status provided by the onSubmit
* @returns - The `IChangeEvent` for the state
*/
function toIChangeEvent<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
state: FormState<T, S, F>,
status?: IChangeEvent['status'],
): IChangeEvent<T, S, F> {
return {
..._pick(state, [
'schema',
'uiSchema',
'fieldPathId',
'schemaUtils',
'formData',
'edit',
'errors',
'errorSchema',
'schemaValidationErrors',
'schemaValidationErrorSchema',
]),
...(status !== undefined ? { status } : undefined),
};
}

/** The definition of a pending change that will be processed in the `onChange` handler
*/
interface PendingChange<T> {
Expand Down Expand Up @@ -330,7 +365,7 @@ export default class Form<

this.state = this.getStateFromProps(props, props.formData);
if (this.props.onChange && !deepEquals(this.state.formData, this.props.formData)) {
this.props.onChange(this.state);
this.props.onChange(toIChangeEvent(this.state));
}
this.formElement = createRef();
}
Expand Down Expand Up @@ -413,7 +448,7 @@ export default class Form<
!deepEquals(nextState.formData, prevState.formData) &&
this.props.onChange
) {
this.props.onChange(nextState);
this.props.onChange(toIChangeEvent(nextState));
}
this.setState(nextState);
}
Expand Down Expand Up @@ -545,7 +580,14 @@ export default class Form<
errorSchema = mergedErrors.errorSchema;
}

const fieldPathId = toFieldPathId('', this.getGlobalFormOptions(this.props));
// Only store a new registry when the props cause a different one to be created
const newRegistry = this.getRegistry(props, rootSchema, schemaUtils);
const registry = deepEquals(state.registry, newRegistry) ? state.registry : newRegistry;
// Only compute a new `fieldPathId` when the `idPrefix` is different than the existing fieldPathId's ID_KEY
const fieldPathId =
state.fieldPathId && state.fieldPathId?.[ID_KEY] === registry.globalFormOptions.idPrefix
? state.fieldPathId
: toFieldPathId('', registry.globalFormOptions);
const nextState: FormState<T, S, F> = {
schemaUtils,
schema: rootSchema,
Expand All @@ -559,6 +601,7 @@ export default class Form<
schemaValidationErrorSchema,
retrievedSchema: _retrievedSchema,
initialDefaultsGenerated: true,
registry,
};
return nextState;
}
Expand Down Expand Up @@ -867,7 +910,7 @@ export default class Form<
}
this.setState(state as FormState<T, S, F>, () => {
if (onChange) {
onChange({ ...this.state, ...state }, id);
onChange(toIChangeEvent({ ...this.state, ...state }), id);
}
// Now remove the change we just completed and call this again
this.pendingChanges.shift();
Expand Down Expand Up @@ -909,7 +952,7 @@ export default class Form<
customErrors: undefined,
} as FormState<T, S, F>;

this.setState(state, () => onChange && onChange({ ...this.state, ...state }));
this.setState(state, () => onChange && onChange(toIChangeEvent({ ...this.state, ...state })));
};

/** Callback function to handle when a field on the form is blurred. Calls the `onBlur` callback for the `Form` if it
Expand Down Expand Up @@ -975,7 +1018,7 @@ export default class Form<
},
() => {
if (onSubmit) {
onSubmit({ ...this.state, formData: newFormData, status: 'submitted' }, event);
onSubmit(toIChangeEvent({ ...this.state, formData: newFormData }, 'submitted'), event);
}
},
);
Expand All @@ -1000,28 +1043,27 @@ export default class Form<
return { idPrefix: rootFieldId || idPrefix, idSeparator, experimental_componentUpdateStrategy };
}

/** Returns the registry for the form */
getRegistry(): Registry<T, S, F> {
const { translateString: customTranslateString, uiSchema = {} } = this.props;
const { schema, schemaUtils } = this.state;
/** Computed the registry for the form using the given `props`, `schema` and `schemaUtils` */
getRegistry(props: FormProps<T, S, F>, schema: S, schemaUtils: SchemaUtilsType<T, S, F>): Registry<T, S, F> {
const { translateString: customTranslateString, uiSchema = {} } = props;
const { fields, templates, widgets, formContext, translateString } = getDefaultRegistry<T, S, F>();
return {
fields: { ...fields, ...this.props.fields },
fields: { ...fields, ...props.fields },
templates: {
...templates,
...this.props.templates,
...props.templates,
ButtonTemplates: {
...templates.ButtonTemplates,
...this.props.templates?.ButtonTemplates,
...props.templates?.ButtonTemplates,
},
},
widgets: { ...widgets, ...this.props.widgets },
widgets: { ...widgets, ...props.widgets },
rootSchema: schema,
formContext: this.props.formContext || formContext,
formContext: props.formContext || formContext,
schemaUtils,
translateString: customTranslateString || translateString,
globalUiOptions: uiSchema[UI_GLOBAL_OPTIONS_KEY],
globalFormOptions: this.getGlobalFormOptions(this.props),
globalFormOptions: this.getGlobalFormOptions(props),
};
}

Expand Down Expand Up @@ -1162,8 +1204,7 @@ export default class Form<
_internalFormWrapper,
} = this.props;

const { schema, uiSchema, formData, errorSchema, fieldPathId } = this.state;
const registry = this.getRegistry();
const { schema, uiSchema, formData, errorSchema, fieldPathId, registry } = this.state;
const { SchemaField: _SchemaField } = registry.fields;
const { SubmitButton } = registry.templates.ButtonTemplates;
// The `semantic-ui` and `material-ui` themes have `_internalFormWrapper`s that take an `as` prop that is the
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/components/fields/ArrayField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -519,8 +519,8 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
const _schemaItems: S = isObject(schema.items) ? (schema.items as S) : ({} as S);
const itemsSchema: S = schemaUtils.retrieveSchema(_schemaItems);
const formData = keyedToPlainFormData(this.state.keyedFormData);
const renderOptionalField = shouldRenderOptionalField(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable(this.props.formData);
const renderOptionalField = shouldRenderOptionalField<T[], S, F>(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable<T[]>(this.props.formData);
const canAdd = this.canAddItem(formData) && (!renderOptionalField || hasFormData);
const actualFormData = hasFormData ? keyedFormData : [];
const extraClass = renderOptionalField ? ' rjsf-optional-array-field' : '';
Expand Down Expand Up @@ -752,8 +752,8 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
const uiOptions = getUiOptions<T[], S, F>(uiSchema);
const { schemaUtils, fields, formContext, globalFormOptions } = registry;
const { OptionalDataControlsField } = fields;
const renderOptionalField = shouldRenderOptionalField(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable(formData);
const renderOptionalField = shouldRenderOptionalField<T[], S, F>(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable<T[]>(formData);
const _schemaItems: S[] = isObject(schema.items) ? (schema.items as S[]) : ([] as S[]);
const itemSchemas = _schemaItems.map((item: S, index: number) =>
schemaUtils.retrieveSchema(item, items[index] as unknown as T[]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ export default function LayoutMultiSchemaField<
displayLabel={displayLabel}
errors={errors}
onChange={onChange}
onKeyRenameBlur={noop}
onRemovePropertyClick={noop}
onDropPropertyClick={ignored}
onKeyChange={ignored}
>
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/fields/MultiSchemaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
globalUiOptions,
);
const isOptionalRender = shouldRenderOptionalField<T, S, F>(registry, schema, required, uiSchema);
const hasFormData = isFormDataAvailable(formData);
const hasFormData = isFormDataAvailable<T>(formData);

const { selectedOption, retrievedOptions } = this.state;
const {
Expand Down
Loading