React-jsonschema-form: Calculated field values

Created on 11 Nov 2017  路  9Comments  路  Source: rjsf-team/react-jsonschema-form

Prerequisites

  • [x] I have read the documentation;
  • [x] In the case of a bug report, I understand that providing a SSCCE example is tremendously useful to the maintainers.

Description

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).

Steps to Reproduce

  1. Create a React component to wrap a Form and maintain it's formData state.
  2. Wire up onChange for the Form to a function that first transforms the current formData and then calls setState with that transformed formData.
  3. Observe how the calculated field values don't render when you'd expect. 馃槥

SSCCE: https://jsfiddle.net/f2y3fq7L/47/

Expected behavior

When I input a value the calculated field(s) update based on said input.

Actual behavior

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!".

Version

1.0.0

bug help wanted

All 9 comments

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>
    );
  }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

pablen picture pablen  路  3Comments

FBurner picture FBurner  路  3Comments

j-zimnowoda picture j-zimnowoda  路  3Comments

ClockerZadq picture ClockerZadq  路  3Comments

sstarrAtmeta picture sstarrAtmeta  路  3Comments