Skip to content

Commit 144f764

Browse files
Fix 391 & 2099 with new initialFormData prop and setFieldValue() on Form (#4815)
* Fix 391 with new initialFormData prop on Form Fixes #391 by adding support for the `initialFormData` prop on `Form` - Updated `@rjsf/core` as follows: - Updated `FormProps` to add the new `initialFormData` prop - Updated `Form` so that is behaves as a "controlled" form when `formData` is passed and uncontrolled when `initialFormData` is passed - Also fixed an issue where live validation was called on the initial form render, causing errors to show immediately, partially fixing #512 - Updated the tests for `Form`, `ArrayField`, `ObjectField` and `StringField` to deal with the change to not live validating on initial render - Added new tests for `Form` that verify the fixes around controlled vs uncontrolled form related to the `initialFormData` and how it affects `reset()` - Updated `form-props.md` and `v6x upgrade guide.md` to document the new `initialFormData` prop and the other potentially breaking `Form` fix - Updated the `CHANGELOG.md` accordingly * Apply suggestions from code review * - Improved documentation * - Fixed blank lines * Update packages/docs/docs/migration-guides/v6.x upgrade guide.md * - Updated `CHANGELOG.md` for other merged PR * - Added `setFieldValue()` implementation on `Form` * - Made `setFieldValue()` pass in the id of the field to `onChange()` * Update packages/core/test/ArrayField.test.jsx Co-authored-by: Nick Grosenbacher <[email protected]> --------- Co-authored-by: Nick Grosenbacher <[email protected]>
1 parent a86aab9 commit 144f764

File tree

10 files changed

+528
-31
lines changed

10 files changed

+528
-31
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,19 @@ should change the heading of the (upcoming) version to include a major version b
2121

2222
- Updated `FormProps` to add new `onChange`/`onBlur` values for the `liveValidate` and `liveOmit` props, deprecating the `boolean` aspect of them
2323
- Updated `Form` to support the new feature to do `onBlur` handling of `liveValidate` and `liveOmit`
24+
- Updated `FormProps` to add the new `initialFormData` prop
25+
- Updated `Form` so that is behaves as a "controlled" form when `formData` is passed and uncontrolled when `initialFormData` is passed, fixing [#391](https://github.com/rjsf-team/react-jsonschema-form/issues/391)
26+
- Also fixed an issue where live validation was called on the initial form render, causing errors to show immediately, partially fixing [#512](https://github.com/rjsf-team/react-jsonschema-form/issues/512)
27+
- Updated `Form` to add a new programmatic function, `setFieldValue(fieldPath: string | FieldPathList, newValue?: T): void`, fixing [#2099](https://github.com/rjsf-team/react-jsonschema-form/issues/2099)
28+
29+
## @rjsf/mantine
30+
31+
- Updated `FieldHelpTemplate` to avoid issue when `help` `and `fieldPathId` are undefined
2432

2533
## Dev / docs / playground
34+
2635
- Updated the playground to switch `liveValidate` and `liveOmit` from checkboxes to radio buttons for the new options
27-
- Updated `form-props.md` and `v6x upgrade guide.md` to document the new feature and deprecation
36+
- Updated `internals.md`, `form-props.md` and `v6x upgrade guide.md` to document the new features, potential breaking changes and deprecations
2837

2938
# 6.0.0-beta.22
3039

@@ -127,6 +136,7 @@ should change the heading of the (upcoming) version to include a major version b
127136
- BREAKING CHANGE: Renamed `ArrayFieldItemButtonsTemplateType` to `ArrayFieldItemButtonsTemplateProps` and updated it to replace the `onAddIndexClick()`, `onCopyIndexClick()`, `onDropIndexClick()` and `onReorderClick()` callback-generator props with the `onAddItem()`, `onCopyItem()`, `onMoveUpItem()`, `onMoveDownItem()` and `onRemoveItem()` callback props
128137

129138
## Dev / docs / playground
139+
130140
- Updated the `formTests.tsx` snapshots to add an `anyOf` of all arrays with different item types and removed the disabling of the optional data controls feature for the optional object with oneOfs
131141
- Updated the snapshots in all of the themes accordingly
132142
- Updated the playground to make the same changes as `formTests.tsx` in the `optionalDataControls.ts` sample, moving the `experimental_defaultFormStateBehavior` inside of a `liveSettings` block

packages/core/src/components/Form.tsx

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ import _toPath from 'lodash/toPath';
5454

5555
import getDefaultRegistry from '../getDefaultRegistry';
5656

57+
/** Internal only symbol used by the `reset()` function to indicate that a reset operation is happening */
58+
const IS_RESET = Symbol('reset');
59+
5760
/** The properties that are passed to the `Form` */
5861
export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> {
5962
/** The JSON schema object for the form */
@@ -64,8 +67,14 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
6467
children?: ReactNode;
6568
/** The uiSchema for the form */
6669
uiSchema?: UiSchema<T, S, F>;
67-
/** The data for the form, used to prefill a form with existing data */
70+
/** The data for the form, used to load a "controlled" form with its current data. If you want an "uncontrolled" form
71+
* with initial data, then use `initialFormData` instead.
72+
*/
6873
formData?: T;
74+
/** The initial data for the form, used to fill an "uncontrolled" form with existing data on the initial render and
75+
* when `reset()` is called programmatically.
76+
*/
77+
initialFormData?: T;
6978
// Form presentation and behavior modifiers
7079
/** You can provide a `formContext` object to the form, which is passed down to all fields and widgets. Useful for
7180
* implementing context aware fields and widgets.
@@ -361,9 +370,11 @@ export default class Form<
361370
throw new Error('A validator is required for Form functionality to work');
362371
}
363372

364-
this.state = this.getStateFromProps(props, props.formData);
365-
if (this.props.onChange && !deepEquals(this.state.formData, this.props.formData)) {
366-
this.props.onChange(toIChangeEvent(this.state));
373+
const { formData: propsFormData, initialFormData, onChange } = props;
374+
const formData = propsFormData ?? initialFormData;
375+
this.state = this.getStateFromProps(props, formData, undefined, undefined, undefined, true);
376+
if (onChange && !deepEquals(this.state.formData, formData)) {
377+
onChange(toIChangeEvent(this.state));
367378
}
368379
this.formElement = createRef();
369380
}
@@ -440,7 +451,6 @@ export default class Form<
440451
) {
441452
if (snapshot.shouldUpdate) {
442453
const { nextState } = snapshot;
443-
444454
if (
445455
!deepEquals(nextState.formData, this.props.formData) &&
446456
!deepEquals(nextState.formData, prevState.formData) &&
@@ -476,6 +486,7 @@ export default class Form<
476486
const schema = 'schema' in props ? props.schema : this.props.schema;
477487
const validator = 'validator' in props ? props.validator : this.props.validator;
478488
const uiSchema: UiSchema<T, S, F> = ('uiSchema' in props ? props.uiSchema! : this.props.uiSchema!) || {};
489+
const isUncontrolled = props.formData === undefined && this.props.formData === undefined;
479490
const edit = typeof inputFormData !== 'undefined';
480491
const liveValidate = 'liveValidate' in props ? props.liveValidate : this.props.liveValidate;
481492
const mustValidate = edit && !props.noValidate && liveValidate;
@@ -506,9 +517,17 @@ export default class Form<
506517
}
507518

508519
const rootSchema = schemaUtils.getRootSchema();
520+
521+
// Compute the formData for getDefaultFormState() function based on the inputFormData, isUncontrolled and state
522+
let defaultsFormData = inputFormData;
523+
if (inputFormData === IS_RESET) {
524+
defaultsFormData = undefined;
525+
} else if (inputFormData === undefined && isUncontrolled) {
526+
defaultsFormData = state.formData;
527+
}
509528
const formData: T = schemaUtils.getDefaultFormState(
510529
rootSchema,
511-
inputFormData,
530+
defaultsFormData,
512531
false,
513532
state.initialDefaultsGenerated,
514533
) as T;
@@ -799,6 +818,20 @@ export default class Form<
799818
return this.getUsedFormData(formData, fieldNames);
800819
};
801820

821+
/** Allows a user to set a value for the provided `fieldPath`, which must be either a dotted path to the field OR a
822+
* `FieldPathList`. To set the root element, used either `''` or `[]` for the path. Passing undefined will clear the
823+
* value in the field.
824+
*
825+
* @param fieldPath - Either a dotted path to the field or the `FieldPathList` to the field
826+
* @param [newValue] - The new value for the field
827+
*/
828+
setFieldValue = (fieldPath: string | FieldPathList, newValue?: T) => {
829+
const { registry } = this.state;
830+
const path = Array.isArray(fieldPath) ? fieldPath : fieldPath.split('.');
831+
const fieldPathId = toFieldPathId('', registry.globalFormOptions, path);
832+
this.onChange(newValue, path, undefined, fieldPathId[ID_KEY]);
833+
};
834+
802835
/** Pushes the given change information into the `pendingChanges` array and then calls `processPendingChanges()` if
803836
* the array only contains a single pending change.
804837
*
@@ -941,8 +974,16 @@ export default class Form<
941974
*
942975
*/
943976
reset = () => {
944-
const { onChange } = this.props;
945-
const newState = this.getStateFromProps(this.props, undefined);
977+
// Cast the IS_RESET symbol to T to avoid type issues, we use this symbol to detect reset mode
978+
const { formData: propsFormData, initialFormData = IS_RESET as T, onChange } = this.props;
979+
const newState = this.getStateFromProps(
980+
this.props,
981+
propsFormData ?? initialFormData,
982+
undefined,
983+
undefined,
984+
undefined,
985+
true,
986+
);
946987
const newFormData = newState.formData;
947988
const state = {
948989
formData: newFormData,

packages/core/test/ArrayField.test.jsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,9 @@ describe('ArrayField', () => {
380380
liveValidate: true,
381381
});
382382

383+
// trigger the errors by submitting the form
384+
submitForm(node);
385+
383386
const matches = node.querySelectorAll('#custom');
384387
expect(matches).to.have.length.of(1);
385388
expect(matches[0].textContent).to.eql('must NOT have fewer than 2 items');
@@ -1302,6 +1305,8 @@ describe('ArrayField', () => {
13021305
formData: ['foo', 'foo'],
13031306
liveValidate: true,
13041307
});
1308+
// trigger the errors by submitting the form since initial render no longer shows them
1309+
submitForm(node);
13051310

13061311
const matches = node.querySelectorAll('#custom');
13071312
expect(matches).to.have.length.of(1);
@@ -1452,6 +1457,9 @@ describe('ArrayField', () => {
14521457
liveValidate: true,
14531458
});
14541459

1460+
// trigger the errors by submitting the form since initial render no longer shows them
1461+
submitForm(node);
1462+
14551463
const matches = node.querySelectorAll('#custom');
14561464
expect(matches).to.have.length.of(1);
14571465
expect(matches[0].textContent).to.eql('must NOT have fewer than 3 items');
@@ -1660,6 +1668,9 @@ describe('ArrayField', () => {
16601668
liveValidate: true,
16611669
});
16621670

1671+
// trigger the errors by submitting the form since initial render no longer shows them
1672+
submitForm(node);
1673+
16631674
const matches = node.querySelectorAll('#custom');
16641675
expect(matches).to.have.length.of(1);
16651676
expect(matches[0].textContent).to.eql('must NOT have fewer than 5 items');
@@ -1733,6 +1744,9 @@ describe('ArrayField', () => {
17331744
liveValidate: true,
17341745
});
17351746

1747+
// trigger the errors by submitting the form since initial render no longer shows them
1748+
submitForm(node);
1749+
17361750
const matches = node.querySelectorAll('#custom-error');
17371751
expect(matches).to.have.length.of(2);
17381752
expect(matches[0].textContent).to.eql('must NOT have fewer than 3 items');
@@ -3136,18 +3150,17 @@ describe('ArrayField', () => {
31363150
it('Check that when formData changes, the form should re-validate', () => {
31373151
const { node, rerender } = createFormComponent({
31383152
schema,
3139-
formData: [
3140-
{
3141-
text: null,
3142-
},
3143-
],
3153+
formData: [{}],
31443154
liveValidate: true,
31453155
});
31463156

3157+
// trigger the errors by submitting the form since initial render no longer shows them
3158+
submitForm(node);
3159+
31473160
const errorMessages = node.querySelectorAll('#root_0_text__error');
31483161
expect(errorMessages).to.have.length(1);
31493162
const errorMessageContent = node.querySelector('#root_0_text__error .text-danger').textContent;
3150-
expect(errorMessageContent).to.contain('must be string');
3163+
expect(errorMessageContent).to.contain("must have required property 'text'");
31513164

31523165
rerender({ schema, formData: [{ text: 'test' }], liveValidate: true });
31533166

0 commit comments

Comments
 (0)