I'm probably totally missing something obvious (maybe something fundamental to React that I'm having to learn the hard way) but I want to be able to calculate the value of a field based on the input of another field but whenever I try to transform the formData via onChange and then setState it doesn't render correctly (or maybe I am not understanding what is actually going on).
Form and maintain it's formData state.onChange for the Form to a function that first transforms the current formData and then calls setState with that transformed formData.SSCCE: https://jsfiddle.net/f2y3fq7L/47/
When I input a value the calculated field(s) update based on said input.
The calculated field values seem to use the previous formData values. (e.g. Given a name field and a greeting field generated from it when you type J then o then h and then n in the name field the greeting is calculated as "" then "Hello J!" then "Hello Jo!" and then "Hello Joh!" instead of "Hello J!" then "Hello Jo!" then "Hello Joh!" and then "Hello John!".
1.0.0
Oh... I think I found the issue. onChange is passed a live view of formData. If I make a deep clone of formData then I get my expected behavior. Hmmmm, should RJSF return a live, mutable view of it's state? That seems dangerous. I wonder if it should instead return a defensive deep clone or an immutable view of some kind. Thoughts?
For reference, here's an SSCCE with the change to get the expected behavior: https://jsfiddle.net/f2y3fq7L/48/
Same issue here, as mfulton26 said, making a deep clone of formData seems to solve, at least at the first level. In case of nested forms (as per "nested" example here) the issue remains, but for nested levels only. My feeling is that it has to be related to the rendering of arrays and objects (not sure about the latter since the first level is working). Still investigating this sorcery
EDIT: nevermind. The inconsistent behaviour between root and nested leveles should have rang some bells much sooner but mondays are always the toughest. To other people who may occur in the same problem, keep in mind that formData need to be deep cloned (as mfulton said) but Object.assign({}, formData) does not a deep clone. Lodash cloneDeep does the job.
Calculated fields should be very common. But seems they didn't get well supported here.
I think this.props.onChange(this.state) is pretty hard to argue with. But I agree that it isn't great when it's possible to meddle with the component's internal data. I'd be OK with returning an immutable view if there were a clear/easy way to do that.
@mfulton26 it seems like this issue is no longer happening with the latest version of rjsf (see https://jsfiddle.net/f2y3fq7L/47/) -- can you confirm?
@mfulton26 it seems like this issue is no longer happening with the latest version of rjsf (see https://jsfiddle.net/f2y3fq7L/47/) -- can you confirm?
It works one event late.
Name: abc
Greeting: Hello ab!
(Newer version of jsfiddle -- the old jsfiddle used the newest version of rjsf, which is incompatible with rjsf 2.0.0-alpha.1, although the same issue with one event late is still there): https://jsfiddle.net/07e4f6co/
Try calling your set state like this to fix the "one event late" issue: https://jsfiddle.net/ernfuwqh/
I built a wrapper component (in TypeScript) that resolves the "one event late" issue. I put a setTimeout() on the calculation, to allow all the events in the form to run before performing the calculation. Here's a working codesandbox demo: https://codesandbox.io/s/calculation-react-json-schema-form-demo-i8x9g
Here's the TypeScript component code:
import React from 'react';
import JSONSchemaForm from '@rjsf/semantic-ui';
import { FormProps, IChangeEvent, ErrorSchema } from '@rjsf/core';
interface CalculationSchemaFormProps<T> extends FormProps<T> {
onCalculate?: (formData: T, updatedField?: keyof T) => T;
}
interface CalculationSchemaFormState<T> {
originalFormData: T | undefined;
formData: T;
}
export default class CalculationSchemaForm<T> extends React.Component<CalculationSchemaFormProps<T>, CalculationSchemaFormState<T>> {
state: CalculationSchemaFormState<T>;
calculating = false;
constructor(props: CalculationSchemaFormProps<T>) {
super(props);
const formData = this.props.formData ? this.props.formData as T : {} as T;
this.state = {
originalFormData: formData,
formData,
};
}
onChange(e: IChangeEvent<T>, es?: ErrorSchema): void {
// Exit if we're calculating
if (this.calculating) { return; }
// Set the calculating flag to true to avoid an infinite loop
this.calculating = true;
// Pass through the new form data to re-render with the updated value
this.setState({ formData: e.formData });
// Get the updated field
const updatedField = this.getUpdatedField(this.state.formData, e.formData);
// Use setTimeout to call onCalculate, to let all the events fire to get everything synched up
setTimeout(() => this.onCalculate(e.formData, updatedField));
// Call the onChange if it was passed in
if (this.props.onChange) {
this.props.onChange(e, es);
}
}
getUpdatedField(oldFormData: any, newFormData: any): keyof T {
// Iterate over the properties to find the changed value
return Object.getOwnPropertyNames(newFormData).find(key => {
// If we have a primitive data type return the comparison
switch (typeof newFormData[key]) {
case 'boolean':
case 'number':
case 'string':
return newFormData[key] !== oldFormData[key];
default:
// If it's undefined or null, return the comparison
if(
newFormData[key] === undefined
|| newFormData[key] === null
) {
return newFormData[key] !== oldFormData[key];
}
// We have either another JSON object or Array
// Return a recursive call to getUpdatedField
return this.getUpdatedField(oldFormData[key], newFormData[key]);
}
}) as keyof T;
}
onCalculate (formData: T, updatedField: keyof T): void {
let updatedFormData = formData;
// Perform the calculation if it was passed in
if (this.props.onCalculate) {
// Get the updated form data
updatedFormData = this.props.onCalculate(formData, updatedField);
}
// Update the form
this.setState({ formData: updatedFormData });
// Set calculating to false
this.calculating = false;
}
render(): React.ReactNode {
// Display the form
return (
<JSONSchemaForm
{...this.props}
schema={this.props.schema}
formData={this.state.formData}
onChange={(e: IChangeEvent<T>, es?: ErrorSchema) => this.onChange(e, es)}
>
{this.props.children}
</JSONSchemaForm>
);
}
}