Skip to content

Commit e98ce8d

Browse files
Feature: Performance optimization, live validate/omit onBlur
Added support to make live validate and live omit work during the `onBlur` phase rather than `onChange` - In `@rjsf/core`: - Updated `FormProps` to add new `onChange`/`onBlur` values for the `liveValidate` and `liveOmit` props, deprecating the `boolean` aspect of them - Updated `Form` to support the new feature to do `onBlur` handling of `liveValidate` and `liveOmit` - Updated the tests to verify the new behavior - Updated the playground to switch `liveValidate` and `liveOmit` from checkboxes to radio buttons for the new options - Updated `form-props.md` and `v6x upgrade guide.md` to document the new feature and deprecation - Updated the `CHANGELOG.md` accordingly
1 parent 4ab0f39 commit e98ce8d

File tree

8 files changed

+141
-36
lines changed

8 files changed

+141
-36
lines changed

CHANGELOG.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ it according to semantic versioning. For example, if your PR adds a breaking cha
1515
should change the heading of the (upcoming) version to include a major version bump.
1616
1717
-->
18+
# 6.0.0-beta.23
19+
20+
## @rjsf/core
21+
22+
- Updated `FormProps` to add new `onChange`/`onBlur` values for the `liveValidate` and `liveOmit` props, deprecating the `boolean` aspect of them
23+
- Updated `Form` to support the new feature to do `onBlur` handling of `liveValidate` and `liveOmit`
24+
25+
## Dev / docs / playground
26+
- 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
28+
1829
# 6.0.0-beta.22
1930

2031
## @rjsf/antd
@@ -44,7 +55,7 @@ should change the heading of the (upcoming) version to include a major version b
4455
- BREAKING CHANGE: Updated `ArrayFieldTemplate` to remove the `ArrayFieldItemTemplate` render in favor of simply using `items` due to `ArrayField` changes
4556
- BREAKING CHANGE: Updated `ArrayFieldItemButtonsTemplate` to replace the old callback-generator functions with the new memoizable callback functions
4657
- Fixed a bug in `Form` to avoid getting errors being reported at the root level via `onChange` when there aren't
47-
- Refactored LayoutGridField as function components instead of a single class component.
58+
- Refactored `LayoutGridField` as function components instead of a single class component.
4859

4960
## @rjsf/daisyui
5061

@@ -127,8 +138,7 @@ should change the heading of the (upcoming) version to include a major version b
127138
- Updated the `utility-functions.md` documentation to add the new `useDeepCompareMemo()` hook
128139
- Updated the `v6.x upgrade guide.md` documentation to add the BREAKING CHANGES to the `ArrayFieldTemplateProps`, `ArrayFieldItemTemplateType`, `ArrayFieldItemButtonsTemplateType`, `FieldTemplateProps`, `ObjectFieldTemplateProps` and `WrapIfAdditionalTemplateProps` interface props changes and the `useDeepCompareMemo()` hook
129140
- Added documentation for the `nameGenerator` prop in `form-props.md` and v6.x upgrade guide
130-
- Updated '@rjsf/snapshot-tests' package to explicitly depend on '@rjsf/core' to build first, fixing an error with parallelized builds
131-
141+
- Updated `@rjsf/snapshot-tests` package to explicitly depend on `@rjsf/core` to build first, fixing an error with parallelized builds
132142

133143
# 6.0.0-beta.21
134144

packages/core/src/components/Form.tsx

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,28 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
168168
* @deprecated - In a future release, this switch may be replaced by making `validator` prop optional
169169
*/
170170
noValidate?: boolean;
171-
/** If set to true, the form will perform validation and show any validation errors whenever the form data is changed,
172-
* rather than just on submit
171+
/** Flag that describes when live validation will be performed. Live validation means that the form will perform
172+
* validation and show any validation errors whenever the form data is updated, rather than just on submit.
173+
*
174+
* If no value (or `false`) is provided, then live validation will not happen. If `true` or `onChange` is provided for
175+
* the flag, then live validation will be performed after processing of all pending changes has completed. If `onBlur`
176+
* is provided, then live validation will be performed when a field that was updated is blurred (as a performance
177+
* optimization).
178+
*
179+
* @deprecated - In a future major release, the `boolean` options for this flag will be removed
173180
*/
174-
liveValidate?: boolean;
175-
/** If `omitExtraData` and `liveOmit` are both set to true, then extra form data values that are not in any form field
176-
* will be removed whenever `onChange` is called. Set to `false` by default
181+
liveValidate?: boolean | 'onChange' | 'onBlur';
182+
/** Flag that describes when live omit will be performed. Live omit happens only when `omitExtraData` is also set to
183+
* to `true` and the form's data is updated by the user.
184+
*
185+
* If no value (or `false`) is provided, then live omit will not happen. If `true` or `onChange` is provided for
186+
* the flag, then live omit will be performed after processing of all pending changes has completed. If `onBlur`
187+
* is provided, then live omit will be performed when a field that was updated is blurred (as a performance
188+
* optimization).
189+
*
190+
* @deprecated - In a future major release, the `boolean` options for this flag will be removed
177191
*/
178-
liveOmit?: boolean;
192+
liveOmit?: boolean | 'onChange' | 'onBlur';
179193
/** If set to true, then extra form data values that are not in any form field will be removed whenever `onSubmit` is
180194
* called. Set to `false` by default.
181195
*/
@@ -834,11 +848,11 @@ export default class Form<
834848
retrievedSchema = newState.retrievedSchema;
835849
}
836850

837-
const mustValidate = !noValidate && liveValidate;
851+
const mustValidate = !noValidate && (liveValidate === true || liveValidate === 'onChange');
838852
let state: Partial<FormState<T, S, F>> = { formData, schema };
839853
let newFormData = formData;
840854

841-
if (omitExtraData === true && liveOmit === true) {
855+
if (omitExtraData === true && (liveOmit === true || liveOmit === 'onChange')) {
842856
newFormData = this.omitExtraData(formData);
843857
state = {
844858
formData: newFormData,
@@ -944,16 +958,52 @@ export default class Form<
944958
};
945959

946960
/** Callback function to handle when a field on the form is blurred. Calls the `onBlur` callback for the `Form` if it
947-
* was provided.
961+
* was provided. Also runs any live validation and/or live omit operations if the flags indicate they should happen
962+
* during `onBlur`.
948963
*
949964
* @param id - The unique `id` of the field that was blurred
950965
* @param data - The data associated with the field that was blurred
951966
*/
952967
onBlur = (id: string, data: any) => {
953-
const { onBlur } = this.props;
968+
const { onBlur, omitExtraData, liveOmit, liveValidate } = this.props;
954969
if (onBlur) {
955970
onBlur(id, data);
956971
}
972+
if ((omitExtraData === true && liveOmit === 'onBlur') || liveValidate === 'onBlur') {
973+
const { onChange, extraErrors } = this.props;
974+
const { formData } = this.state;
975+
let newFormData: T | undefined = formData;
976+
let state: Partial<FormState<T, S, F>> = { formData: newFormData };
977+
if (omitExtraData === true && liveOmit === 'onBlur') {
978+
newFormData = this.omitExtraData(formData);
979+
state = { formData: newFormData };
980+
}
981+
if (liveValidate === 'onBlur') {
982+
const { schema, schemaUtils, errorSchema, customErrors, retrievedSchema } = this.state;
983+
const liveValidation = this.liveValidate(
984+
schema,
985+
schemaUtils,
986+
errorSchema,
987+
newFormData,
988+
extraErrors,
989+
customErrors,
990+
retrievedSchema,
991+
);
992+
state = { formData: newFormData, ...liveValidation, customErrors };
993+
}
994+
const hasChanges = Object.keys(state).some((key) => {
995+
const oldData = _get(this.state, key);
996+
const newData = _get(state, key);
997+
return !deepEquals(oldData, newData);
998+
});
999+
if (hasChanges) {
1000+
this.setState(state as FormState<T, S, F>, () => {
1001+
if (onChange) {
1002+
onChange(toIChangeEvent({ ...this.state, ...state }), id);
1003+
}
1004+
});
1005+
}
1006+
}
9571007
};
9581008

9591009
/** Callback function to handle when a field on the form is focused. Calls the `onFocus` callback for the `Form` if it

packages/core/test/Form.test.jsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2089,7 +2089,6 @@ describeRepeated('Form common', (createFormComponent) => {
20892089
const formProps = {
20902090
ref: createRef(),
20912091
schema: { type: 'string' },
2092-
liveValidate: true,
20932092
};
20942093

20952094
it('should call submit handler with new formData prop value', async () => {
@@ -2184,7 +2183,6 @@ describeRepeated('Form common', (createFormComponent) => {
21842183
const formProps = {
21852184
ref: createRef(),
21862185
schema: { type: 'string' },
2187-
liveValidate: true,
21882186
};
21892187
const { node, onChange } = createFormComponent(formProps);
21902188

@@ -2651,7 +2649,7 @@ describeRepeated('Form common', (createFormComponent) => {
26512649
});
26522650
});
26532651

2654-
describe('root level', () => {
2652+
describe('root level, live validation', () => {
26552653
const formProps = {
26562654
liveValidate: true,
26572655
schema: {
@@ -2687,7 +2685,7 @@ describeRepeated('Form common', (createFormComponent) => {
26872685
});
26882686
});
26892687

2690-
describe('root level with multiple errors', () => {
2688+
describe('root level with multiple errors, live validation', () => {
26912689
const formProps = {
26922690
liveValidate: true,
26932691
schema: {
@@ -2733,7 +2731,7 @@ describeRepeated('Form common', (createFormComponent) => {
27332731
});
27342732
});
27352733

2736-
describe('nested field level', () => {
2734+
describe('nested field level, live validation', () => {
27372735
const schema = {
27382736
type: 'object',
27392737
properties: {
@@ -2785,7 +2783,7 @@ describeRepeated('Form common', (createFormComponent) => {
27852783
});
27862784
});
27872785

2788-
describe('array indices', () => {
2786+
describe('array indices, live validation', () => {
27892787
const schema = {
27902788
type: 'array',
27912789
items: {
@@ -2837,7 +2835,7 @@ describeRepeated('Form common', (createFormComponent) => {
28372835
});
28382836
});
28392837

2840-
describe('nested array indices', () => {
2838+
describe('nested array indices, live validation', () => {
28412839
const schema = {
28422840
type: 'object',
28432841
properties: {
@@ -2898,7 +2896,7 @@ describeRepeated('Form common', (createFormComponent) => {
28982896
});
28992897
});
29002898

2901-
describe('nested arrays', () => {
2899+
describe('nested arrays, live validation', () => {
29022900
const schema = {
29032901
type: 'object',
29042902
properties: {
@@ -2976,7 +2974,7 @@ describeRepeated('Form common', (createFormComponent) => {
29762974
});
29772975
});
29782976

2979-
describe('array nested items', () => {
2977+
describe('array nested items, live validation', () => {
29802978
const schema = {
29812979
type: 'array',
29822980
items: {
@@ -3025,7 +3023,7 @@ describeRepeated('Form common', (createFormComponent) => {
30253023
});
30263024
});
30273025

3028-
describe('schema dependencies', () => {
3026+
describe('schema dependencies, live validation', () => {
30293027
const schema = {
30303028
type: 'object',
30313029
properties: {
@@ -3144,7 +3142,7 @@ describeRepeated('Form common', (createFormComponent) => {
31443142
});
31453143
});
31463144

3147-
describe('customValidate errors', () => {
3145+
describe('customValidate errors, live validation', () => {
31483146
it('customValidate should raise an error when End is larger than Start field.', () => {
31493147
let schema = {
31503148
required: ['Start', 'End'],
@@ -3466,7 +3464,7 @@ describeRepeated('Form common', (createFormComponent) => {
34663464
});
34673465
});
34683466

3469-
describe('Custom format updates', () => {
3467+
describe('Custom format updates, live validation', () => {
34703468
it('Should update custom formats when customFormats is changed', () => {
34713469
const formProps = {
34723470
ref: createRef(),

packages/docs/docs/api-reference/form-props.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,11 +482,27 @@ You can also create a custom generator by implementing the `NameGeneratorFunctio
482482

483483
## liveOmit
484484

485-
If `omitExtraData` and `liveOmit` are both set to true, then extra form data values that are not in any form field will be removed whenever `onChange` is called. Set to `false` by default.
485+
Flag that describes when live omit will be performed. Live omit happens only when `omitExtraData` is also set to
486+
to `true` and the form's data is updated by the user.
487+
488+
If no value (or `false`) is provided, then live omit will not happen. If `true` or `onChange` is provided for
489+
the flag, then live omit will be performed after processing of all pending changes has completed. If `onBlur`
490+
is provided, then live omit will be performed when a field that was updated is blurred (as a performance
491+
optimization).
492+
493+
> NOTE: The `boolean` options for this flag is deprecated and will be removed in a future major release
486494
487495
## liveValidate
488496

489-
If set to true, the form will perform validation and show any validation errors whenever the form data is changed, rather than just on submit.
497+
Flag that describes when live validation will be performed. Live validation means that the form will perform
498+
validation and show any validation errors whenever the form data is updated, rather than just on submit.
499+
500+
If no value (or `false`) is provided, then live validation will not happen. If `true` or `onChange` is provided for
501+
the flag, then live validation will be performed after processing of all pending changes has completed. If `onBlur`
502+
is provided, then live validation will be performed when a field that was updated is blurred (as a performance
503+
optimization).
504+
505+
> NOTE: The `boolean` options for this flag is deprecated and will be removed in a future major release
490506
491507
## method
492508

@@ -504,6 +520,8 @@ If set to true, turns off HTML5 validation on the form. Set to `false` by defaul
504520

505521
If set to true, turns off all validation. Set to `false` by default.
506522

523+
> NOTE: In a future major release, this flag may be deleted replaced by making `validator` prop optional
524+
507525
## omitExtraData
508526

509527
If set to true, then extra form data values that are not in any form field will be removed whenever `onSubmit` is called. Set to `false` by default.

packages/docs/docs/migration-guides/v6.x upgrade guide.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,15 @@ Three new validator-based utility functions are available in `@rjsf/utils`:
961961
- This new optional flag was added to the `SchemaUtilsType` interface's version of `retrieveSchema()` as well.
962962
- `validationDataMerge()`: Added optional `preventDuplicates` boolean flag that causes the `mergeObjects()` call to receive `preventDuplicates` instead of `true`
963963

964+
### liveValidate and liveOmit performance improvement
965+
966+
Because `liveValidate` and `liveOmit` can have serious performance impacts in the `Form`, those flags were updated to add the following new options:
967+
968+
- 'onChange' - Live validate and live omit will happen as part of the `onChange` processing (the same as setting the flag to `true`)
969+
- 'onBlur' - Live validate and live omit will happen as part of the `onBlur` processing, saving the computations until the user is done inputting data. If any changes to the state occur due to this processing, the `onChange()` event will be sent back to the client with the new data.
970+
971+
> NOTE: Due to this new feature, the old `boolean` options on those two flag has been deprecated. In a future major release, you will only be able to pass `onChange`, `onBlur` to activate the feature or `undefined` to disable it
972+
964973
### Optional Data Controls
965974

966975
RJSF 6.x introduces a new feature that allows developers to provide a condensed UI for users who don't care to enter an optional list of array items or set of optional object fields.

packages/playground/src/components/Header.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,26 @@ function HeaderButtons({ playGroundFormRef }: { playGroundFormRef: MutableRefObj
6262
const liveSettingsBooleanSchema: RJSFSchema = {
6363
type: 'object',
6464
properties: {
65-
liveValidate: { type: 'boolean', title: 'Live validation' },
6665
disabled: { type: 'boolean', title: 'Disable whole form' },
6766
readonly: { type: 'boolean', title: 'Readonly whole form' },
6867
omitExtraData: { type: 'boolean', title: 'Omit extra data' },
69-
liveOmit: { type: 'boolean', title: 'Live omit' },
7068
noValidate: { type: 'boolean', title: 'Disable validation' },
7169
noHtml5Validate: { type: 'boolean', title: 'Disable HTML 5 validation' },
7270
focusOnFirstError: { type: 'boolean', title: 'Focus on 1st Error' },
73-
experimental_componentUpdateStrategy: {
74-
type: 'string',
75-
title: 'Component update strategy',
76-
default: 'customDeep',
77-
enum: ['customDeep', 'shallow', 'always'],
78-
},
71+
liveValidate: { type: 'string', title: 'Live validation', default: false, enum: [false, 'onChange', 'onBlur'] },
72+
liveOmit: { type: 'string', title: 'Live omit', default: false, enum: [false, 'onChange', 'onBlur'] },
7973
showErrorList: {
8074
type: 'string',
8175
default: 'top',
8276
title: 'Show Error List',
8377
enum: [false, 'top', 'bottom'],
8478
},
79+
experimental_componentUpdateStrategy: {
80+
type: 'string',
81+
title: 'Component update strategy',
82+
default: 'customDeep',
83+
enum: ['customDeep', 'shallow', 'always'],
84+
},
8585
},
8686
};
8787

@@ -221,6 +221,18 @@ const liveSettingsBooleanUiSchema: UiSchema = {
221221
inline: true,
222222
},
223223
},
224+
liveValidate: {
225+
'ui:widget': 'radio',
226+
'ui:options': {
227+
inline: true,
228+
},
229+
},
230+
liveOmit: {
231+
'ui:widget': 'radio',
232+
'ui:options': {
233+
inline: true,
234+
},
235+
},
224236
};
225237

226238
const liveSettingsSelectUiSchema: UiSchema = {

packages/playground/src/components/Playground.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ export default function Playground({ themes, validators }: PlaygroundProps) {
110110
setFormData(formData);
111111
setExtraErrors(extraErrors);
112112
setShowForm(true);
113+
if (liveSettings?.liveValidate === true) {
114+
// Convert v5 true value to `onChange`
115+
liveSettings.liveValidate = 'onChange';
116+
}
117+
if (liveSettings?.liveOmit === true) {
118+
// Convert v5 true value to `onChange`
119+
liveSettings.liveOmit = 'onChange';
120+
}
113121
setLiveSettings(liveSettings);
114122
if ('validator' in data && theValidator !== undefined) {
115123
setValidator(theValidator);

packages/playground/src/samples/simple.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const simple: Sample = {
5050
'ui:autocomplete': 'given-name',
5151
'ui:enableMarkdownInDescription': true,
5252
'ui:description':
53-
'Make things **bold** or *italic*. Embed snippets of `code`. <small>And this is a small texts.</small> ',
53+
'Make things **bold** or *italic*. Embed snippets of `code`. <small>NOTE: Unsafe HTML, not rendered</small> ',
5454
},
5555
age: {
5656
'ui:widget': 'updown',

0 commit comments

Comments
 (0)