-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: MNP of debounced validation #1
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,11 +38,14 @@ import _get from 'lodash/get'; | |
import _isEmpty from 'lodash/isEmpty'; | ||
import _pick from 'lodash/pick'; | ||
import _toPath from 'lodash/toPath'; | ||
import _debounce from 'lodash/debounce'; | ||
import _memoize from 'lodash/memoize'; | ||
|
||
import getDefaultRegistry from '../getDefaultRegistry'; | ||
|
||
/** The properties that are passed to the `Form` */ | ||
export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> { | ||
enableLogging?: boolean; | ||
/** The JSON schema object for the form */ | ||
schema: S; | ||
/** An implementation of the `ValidatorType` interface that is needed for form validation to work */ | ||
|
@@ -156,7 +159,7 @@ export interface FormProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F e | |
/** 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 | ||
*/ | ||
liveValidate?: boolean; | ||
liveValidate?: boolean | { debounceThreshold: number }; | ||
/** 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 | ||
*/ | ||
|
@@ -274,9 +277,17 @@ export default class Form< | |
if (this.props.onChange && !deepEquals(this.state.formData, this.props.formData)) { | ||
this.props.onChange(this.state); | ||
} | ||
|
||
this.validateAndUpdateState(this.state.formData); | ||
this.formElement = createRef(); | ||
} | ||
|
||
log = (...params: any[]) => { | ||
if (this.props.enableLogging) { | ||
console.log(...params); | ||
} | ||
}; | ||
|
||
/** React lifecycle method that gets called before new props are provided, updates the state based on new props. It | ||
* will also call the`onChange` handler if the `formData` is modified to add missing default values as part of the | ||
* state construction. | ||
|
@@ -285,14 +296,30 @@ export default class Form< | |
*/ | ||
UNSAFE_componentWillReceiveProps(nextProps: FormProps<T, S, F>) { | ||
const nextState = this.getStateFromProps(nextProps, nextProps.formData); | ||
if ( | ||
const mustValidate = !nextProps.noValidate && nextProps.liveValidate; | ||
const shouldCallOnChange = | ||
!deepEquals(nextState.formData, nextProps.formData) && | ||
!deepEquals(nextState.formData, this.state.formData) && | ||
nextProps.onChange | ||
) { | ||
nextProps.onChange(nextState); | ||
nextProps.onChange; | ||
|
||
if (shouldCallOnChange) { | ||
nextProps.onChange!(nextState); | ||
} | ||
|
||
this.setState(nextState); | ||
|
||
if (mustValidate) { | ||
// @ts-ignore | ||
const debounceThreshold = nextProps.liveValidate?.debounceThreshold; | ||
|
||
const callbackAfterValidation = () => { | ||
if (shouldCallOnChange) { | ||
nextProps.onChange!(this.state); | ||
} | ||
}; | ||
|
||
this.validateAndUpdateStateDebounced(debounceThreshold)(nextState.formData, callbackAfterValidation); | ||
} | ||
} | ||
|
||
/** Extracts the updated state from the given `props` and `inputFormData`. As part of this process, the | ||
|
@@ -308,8 +335,6 @@ export default class Form< | |
const schema = 'schema' in props ? props.schema : this.props.schema; | ||
const uiSchema: UiSchema<T, S, F> = ('uiSchema' in props ? props.uiSchema! : this.props.uiSchema!) || {}; | ||
const edit = typeof inputFormData !== 'undefined'; | ||
const liveValidate = 'liveValidate' in props ? props.liveValidate : this.props.liveValidate; | ||
const mustValidate = edit && !props.noValidate && liveValidate; | ||
const rootSchema = schema; | ||
const experimental_defaultFormStateBehavior = | ||
'experimental_defaultFormStateBehavior' in props | ||
|
@@ -328,38 +353,25 @@ export default class Form< | |
const getCurrentErrors = (): ValidationData<T> => { | ||
if (props.noValidate) { | ||
return { errors: [], errorSchema: {} }; | ||
} else if (!props.liveValidate) { | ||
return { | ||
errors: state.schemaValidationErrors || [], | ||
errorSchema: state.schemaValidationErrorSchema || {}, | ||
}; | ||
} | ||
|
||
// return { | ||
// errors: state.schemaValidationErrors || [], | ||
// errorSchema: state.schemaValidationErrorSchema || {}, | ||
// }; | ||
|
||
return { | ||
errors: state.errors || [], | ||
errorSchema: state.errorSchema || {}, | ||
}; | ||
}; | ||
|
||
let errors: RJSFValidationError[]; | ||
let errorSchema: ErrorSchema<T> | undefined; | ||
let schemaValidationErrors: RJSFValidationError[] = state.schemaValidationErrors; | ||
let schemaValidationErrorSchema: ErrorSchema<T> = state.schemaValidationErrorSchema; | ||
if (mustValidate) { | ||
const schemaValidation = this.validate(formData, schema, schemaUtils); | ||
errors = schemaValidation.errors; | ||
errorSchema = schemaValidation.errorSchema; | ||
schemaValidationErrors = errors; | ||
schemaValidationErrorSchema = errorSchema; | ||
} else { | ||
const currentErrors = getCurrentErrors(); | ||
errors = currentErrors.errors; | ||
errorSchema = currentErrors.errorSchema; | ||
} | ||
if (props.extraErrors) { | ||
const merged = validationDataMerge({ errorSchema, errors }, props.extraErrors); | ||
errorSchema = merged.errorSchema; | ||
errors = merged.errors; | ||
} | ||
const currentErrors = getCurrentErrors(); | ||
const errors: RJSFValidationError[] = currentErrors.errors; | ||
const errorSchema: ErrorSchema<T> | undefined = currentErrors.errorSchema; | ||
const schemaValidationErrors: RJSFValidationError[] = state.schemaValidationErrors; | ||
const schemaValidationErrorSchema: ErrorSchema<T> = state.schemaValidationErrorSchema; | ||
|
||
const idSchema = schemaUtils.toIdSchema( | ||
retrievedSchema, | ||
uiSchema['ui:rootFieldId'], | ||
|
@@ -508,49 +520,70 @@ export default class Form< | |
|
||
const mustValidate = !noValidate && liveValidate; | ||
let state: Partial<FormState<T, S, F>> = { formData, schema }; | ||
let newFormData = formData; | ||
|
||
if (omitExtraData === true && liveOmit === true) { | ||
const retrievedSchema = schemaUtils.retrieveSchema(schema, formData); | ||
const pathSchema = schemaUtils.toPathSchema(retrievedSchema, '', formData); | ||
|
||
const fieldNames = this.getFieldNames(pathSchema, formData); | ||
|
||
newFormData = this.getUsedFormData(formData, fieldNames); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not keep this change since the state update below on 541 will be the old form data before omitting extra data? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean that this is a mistake, and L541 should get |
||
state = { | ||
formData: newFormData, | ||
formData: this.getUsedFormData(formData, fieldNames), | ||
}; | ||
} | ||
|
||
if (mustValidate) { | ||
const schemaValidation = this.validate(newFormData); | ||
let errors = schemaValidation.errors; | ||
let errorSchema = schemaValidation.errorSchema; | ||
const schemaValidationErrors = errors; | ||
const schemaValidationErrorSchema = errorSchema; | ||
if (extraErrors) { | ||
const merged = validationDataMerge(schemaValidation, extraErrors); | ||
errorSchema = merged.errorSchema; | ||
errors = merged.errors; | ||
} | ||
state = { | ||
formData: newFormData, | ||
errors, | ||
errorSchema, | ||
schemaValidationErrors, | ||
schemaValidationErrorSchema, | ||
}; | ||
} else if (!noValidate && newErrorSchema) { | ||
if (!noValidate && newErrorSchema) { | ||
const errorSchema = extraErrors | ||
? (mergeObjects(newErrorSchema, extraErrors, 'preventDuplicates') as ErrorSchema<T>) | ||
: newErrorSchema; | ||
|
||
state = { | ||
formData: newFormData, | ||
formData, | ||
errorSchema: errorSchema, | ||
errors: toErrorList(errorSchema), | ||
}; | ||
} | ||
this.setState(state as FormState<T, S, F>, () => onChange && onChange({ ...this.state, ...state }, id)); | ||
|
||
this.setState(state as FormState<T, S, F>, () => { | ||
onChange && onChange({ ...this.state, ...state }, id); | ||
|
||
if (mustValidate) { | ||
// @ts-ignore | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are Typescript error is being disabled? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No idea, this is just to get it to compile, I would resolve all TS issues in the final PR |
||
const debounceThreshold = liveValidate?.debounceThreshold; | ||
const callbackAfterValidation = () => this.props.onChange?.(this.state, id); | ||
this.validateAndUpdateStateDebounced(debounceThreshold)(state.formData, callbackAfterValidation); | ||
} | ||
}); | ||
}; | ||
|
||
validateAndUpdateStateDebounced = _memoize((debounceThreshold: number | null | undefined) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make more sense to use the react version of memoize? or possibly create an instance variable for the debounce function and only change it when the debounceThreshold changes/is removed? Then you create the function in the constructor and in the componentDidUpdate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what you mean by "the react version of memoize", but using a single-line cache memoize implementation (something like memoize-one) would probably lead to a easier-to-read code :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean |
||
return typeof debounceThreshold === 'number' | ||
? (_debounce(this.validateAndUpdateState, debounceThreshold) as any) | ||
: this.validateAndUpdateState; | ||
}); | ||
|
||
validateAndUpdateState = (formData: T | undefined, callback?: () => void) => { | ||
this.log('RUNNING VALIDATE!!', this.props.liveValidate); | ||
const schemaValidation = this.validate(formData); | ||
let errors = schemaValidation.errors; | ||
let errorSchema = schemaValidation.errorSchema; | ||
const schemaValidationErrors = errors; | ||
const schemaValidationErrorSchema = errorSchema; | ||
|
||
if (this.props.extraErrors) { | ||
const merged = validationDataMerge(schemaValidation, this.props.extraErrors); | ||
errorSchema = merged.errorSchema; | ||
errors = merged.errors; | ||
} | ||
|
||
const state = { | ||
errors, | ||
errorSchema, | ||
schemaValidationErrors, | ||
schemaValidationErrorSchema, | ||
}; | ||
|
||
this.setState(state as FormState<T, S, F>, callback); | ||
}; | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about the removal of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in the issue thread, this is not a real PR, just asking for validation on the generation direction:
This was just a hack to get Husky to let me push. Maybe there's a easier way to do this, I've never worked with it before, but this seemed like an obvious way to shut him up :D