16

I'm trying to build a form with conditional fields from a JSON schema using react-jsonschema-form and react-jsonschem-form-conditionals.

The components I'm rendering are a FormWithConditionals and a FormModelInspector. The latter is a very simple component that shows the form model.

screen shot 2018-02-01 at 17 50 32

The relevant source code is:

import React from 'react';
import PropTypes from 'prop-types';
import Engine from "json-rules-engine-simplified";
import Form from "react-jsonschema-form";
import applyRules from "react-jsonschema-form-conditionals";

function FormModelInspector (props) {

  return (
    <div>
      <div className="checkbox">
        <label>
          <input type="checkbox" onChange={props.onChange} checked={props.showModel}/>
          Show Form Model
        </label>
      </div>
      {
        props.showModel && <pre>{JSON.stringify(props.formData, null, 2)}</pre>
      }
    </div>
  )
}

class ConditionalForm extends React.Component {

  constructor (props) {
    super(props);
    this.state = {
      formData: {},
      showModel: true
    };
    this.handleFormDataChange = this.handleFormDataChange.bind(this);
    this.handleShowModelChange = this.handleShowModelChange.bind(this);
  }

  handleShowModelChange (event) {
    this.setState({showModel: event.target.checked});
  }

  handleFormDataChange ({formData}) {
    this.setState({formData});
  }

  render () {
    const schema = {
      type: "object",
      title: "User form",
      properties: {
        nameHider: {
          type: 'boolean',
          title: 'Hide name'
        },
        name: {
          type: 'string',
          title: 'Name'
        }
      }
    };

    const uiSchema = {};

    const rules = [{
      conditions: {
        nameHider: {is: true}
      },
      event: {
        type: "remove",
        params: {
          field: "name"
        }
      }
    }];

    const FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);

    return (
      <div className="row">
        <div className="col-md-6">
          <FormWithConditionals schema={schema}
                uiSchema={uiSchema}
                formData={this.state.formData}
                onChange={this.handleFormDataChange}
                noHtml5Validate={true}>
          </FormWithConditionals>
        </div>
        <div className="col-md-6">
          <FormModelInspector formData={this.state.formData}
                              showModel={this.state.showModel}
                              onChange={this.handleShowModelChange}/>
        </div>
      </div>
    );
  }
}

ConditionalForm.propTypes = {
  schema: PropTypes.object.isRequired,
  uiSchema: PropTypes.object.isRequired,
  rules: PropTypes.array.isRequired
};

ConditionalForm.defaultProps = {
  uiSchema: {},
  rules: []
};

However, every time I change a field's value, the field loses focus. I suspect the cause of the problem is something in the react-jsonschema-form-conditionals library, because if I replace <FormWithConditionals> with <Form>, the problem does not occur.

If I remove the handler onChange={this.handleFormDataChange} the input field no longer loses focus when it's value changes (but removing this handler breaks the FormModelInspector).

Aside

In the code above, if I remove the handler onChange={this.handleFormDataChange}, the <FormModelInspector> is not updated when the form data changes. I don't understand why this handler is necessary because the <FormModelInspector> is passed a reference to the form data via the formData attribute. Perhaps it's because every change to the form data causes a new object to be constructed, rather than a modification of the same object?

Dónal
  • 185,044
  • 174
  • 569
  • 824

3 Answers3

11

The problem is pretty straightforward, you are creating a FormWithConditionals component in your render method and in your onChange handler you setState which triggers a re-render and thus a new instance of FormWithConditionals is created and hence it loses focus. You need to move this instance out of render method and perhaps out of the component itself since it uses static values.

As schema, uiSchema and rules are passed as props to the ConditionalForm, you can create an instance of FormWithConditionals in constructor function and use it in render like this

    import React from 'react';
    import PropTypes from 'prop-types';
    import Engine from "json-rules-engine-simplified";
    import Form from "react-jsonschema-form";
    import applyRules from "react-jsonschema-form-conditionals";

    function FormModelInspector (props) {

      return (
        <div>
          <div className="checkbox">
            <label>
              <input type="checkbox" onChange={props.onChange} checked={props.showModel}/>
              Show Form Model
            </label>
          </div>
          {
            props.showModel && <pre>{JSON.stringify(props.formData, null, 2)}</pre>
          }
        </div>
      )
    }


    class ConditionalForm extends React.Component {

      constructor (props) {
        super(props);
        this.state = {
          formData: {},
          showModel: true
        };
        const { schema, uiSchema, rules } = props;
        this.FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);
        this.handleFormDataChange = this.handleFormDataChange.bind(this);
        this.handleShowModelChange = this.handleShowModelChange.bind(this);
      }

      handleShowModelChange (event) {
        this.setState({showModel: event.target.checked});
      }

      handleFormDataChange ({formData}) {
        this.setState({formData});
      }

      render () {
        const FormWithConditionals = this.FormWithConditionals;
        return (
          <div className="row">
            <div className="col-md-6">
              <FormWithConditionals schema={schema}
                    uiSchema={uiSchema}
                    formData={this.state.formData}
                    onChange={this.handleFormDataChange}
                    noHtml5Validate={true}>
              </FormWithConditionals>
            </div>
            <div className="col-md-6">
              <FormModelInspector formData={this.state.formData}
                                  showModel={this.state.showModel}
                                  onChange={this.handleShowModelChange}/>
            </div>
          </div>
        );
      }
    }

    ConditionalForm.propTypes = {
      schema: PropTypes.object.isRequired,
      uiSchema: PropTypes.object.isRequired,
      rules: PropTypes.array.isRequired
    };

    ConditionalForm.defaultProps = {
      uiSchema: {},
      rules: []
    };
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Thanks for the answer. I'll try out your suggestion tomorrow and accept your answer if it works. Can you explain what you mean by "it uses static values like"? – Dónal Feb 04 '18 at 17:33
  • apply rules takes object whose values are simple constant strings and none of it depends on a variable – Shubham Khatri Feb 04 '18 at 17:35
  • This change you proposed - moving `applyRules` outside the `ConditionalForm` component - doesn't work because the `schema`, `uiSchema` and `rules` arguments of `applyRules` are props of the `ConditionalForm` component. – Dónal Feb 05 '18 at 11:10
  • In your question, you have mentioned them as constant right?? – Shubham Khatri Feb 05 '18 at 11:15
  • `schema`, `uiSchema` and `rules` are constants, if that's what you're asking? – Dónal Feb 05 '18 at 11:17
  • @Dónal, yes that is that I am asking, they are defined as const in ConditionalForm and not as props to it – Shubham Khatri Feb 05 '18 at 11:18
  • Let me know if you are still not able to solve the problem – Shubham Khatri Feb 05 '18 at 11:36
  • Your updated answer solved the problem, thanks a lot – Dónal Feb 05 '18 at 11:40
  • componentWillMount is deprecated since React 16.3 – ml242 May 09 '18 at 16:03
  • @ml242 , Thanks, I updated my answer for the same. However, componentWillMount is not deprecated in version 16.3, It can still be used and only from version 17 is react planning to deprecate it – Shubham Khatri May 09 '18 at 16:20
  • I've tried this solution but I struggle with the form loosing focus on the first change, e.g. entering a letter in a field (after I then resume focus manually, it works as intended). Any idea what might be driving this? – mfcss Aug 26 '20 at 07:53
2

For anyone bumping into the same problem but using Hooks, here's how without a class :

Just use a variable declared outside the component and initialize it inside useEffect. (don't forget to pass [] as second parameter to tell react that we do not depend on any variable, replicating the componentWillMount effect)

// import ...
import Engine from 'json-rules-engine-simplified'
import Form from 'react-jsonschema-form'

let FormWithConditionals = () => null

const MyComponent = (props) => {
  const {
    formData,
    schema,
    uischema,
    rules,
  } = props;

  useEffect(() => {
    FormWithConditionals = applyRules(schema, uischema, rules, Engine)(Form)
  }, [])

  return (
    <FormWithConditionals>
      <div></div>
    </FormWithConditionals>
  );
}

export default MyComponent
Eric Martin
  • 2,247
  • 1
  • 16
  • 16
0

Have you tried declaring function FormModelInspector as an arrow func :

const FormModelInspector = props => (
    <div>
      <div className="checkbox">
        <label>
          <input type="checkbox" onChange={props.onChange} checked={props.showModel}/>
          Show Form Model
        </label>
      </div>
      {
        props.showModel && <pre>{JSON.stringify(props.formData, null, 2)}</pre>
      }
    </div>
  )
Dyo
  • 4,429
  • 1
  • 15
  • 30
  • @Dónal I maybe misunderstood what you said in your 'Aside', but declaring this function as a stateless component or a pureComponent can prevent some unnecessary render(). Actually i think your issue is the way you construct your FormWithConditionals, i'll update my answer if the other one don't work for you. – Dyo Feb 05 '18 at 11:36
  • Redefining `FormModelInspector` as an arrow function didn't change anything – Dónal Feb 05 '18 at 11:39