Force input's onChange event bubble to parent form with the value stored in the state
Asked Answered
A

3

6

EDIT

Sorry for showing wrong use-case. All inputs inside the Form are being passed though this.props.children, and they can be situated at any deep point of the components tree, so the approach of passing the handleChange directly to inputs will not work at all.


Here is code snippet with the reproduction of the problem.

class CustomSelect extends React.Component {
  items = [
    { id: 1, text: "Kappa 1" },
    { id: 2, text: "Kappa 2" },
    { id: 3, text: "Kappa 3" }
  ]
  
  state = {
    selected: null,
  }
  
  handleSelect = (item) => {
    this.setState({ selected: item })
  }
  
  render() {
    var { selected } = this.state
    return (
      <div className="custom-select">
        <input
          name={this.props.name}
          required
          style={{ display: "none" }} // or type="hidden", whatever
          value={selected
            ? selected.id
            : ""
          }
          onChange={() => {}}
        />
        <div>Selected: {selected ? selected.text : "nothing"}</div>
        {this.items.map(item => {
          return (
            <button 
              key={item.id}
              type="button" 
              onClick={() => this.handleSelect(item)}
            >
              {item.text}
            </button>
          )
        })}
      </div>
    )
  }
}

class Form extends React.Component {
  handleChange = (event) => {
    console.log("Form onChange")
  }
  
  render() {
    return (
      <form onChange={this.handleChange}>
        {this.props.children}
      </form>
    )
  }
}

ReactDOM.render(
  <Form>
    <label>This input will trigger form's onChange event</label>
    <input />
    <CustomSelect name="kappa" />
  </Form>,
  document.getElementById("__root")
 )
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>


<div id="__root"></div>

As you can see, when you type something in default input (controlled or uncontrolled, whatever), form catches bubbled onChange event. But when you are setting the value of the input programmatically (with the state, in this case), the onChange event is not being triggered, so I cannot catch this changes inside the form's onChange.

Is there any options to beat this problem? I tried to input.dispatchEvent(new Event("change", { bubbles: true })) immediately after setState({ selected: input }) and inside it's callback, but there is no result.

Armbruster answered 11/4, 2019 at 21:3 Comment(3)
Why are you passing onChange to the form instead of the input?Coffle
@Coffle because I want to track all changes of the form's inputs.Armbruster
@Coffle I just realized, what are you talking about. I show the wrong use-case in my snipped. Inputs are being passed to the Form through children. I have edited my code snippet. Sorry for that.Armbruster
R
3

Update your CustomSelect component with the following:

class CustomSelect extends React.Component {

    ...

    // you'll use this reference to access the html input.
    ref = React.createRef();

    handleSelect = item => {
        this.setState({ selected: item });

        // React overrides input value setter, but you can call the
        // function directly on the input as context
        const inputValSetter = Object.getOwnPropertyDescriptor(
            window.HTMLInputElement.prototype,
            "value"
        ).set;
        inputValSetter.call(this.ref.current, "dummy");

        // fire event
        const ev = new Event("input", { bubbles: true });
        this.ref.current.dispatchEvent(ev);
    };

    ...

    render() {
        ...

        return (
            <div className="custom-select">
                <input
                    // you'll use the reference in `handleSelect`
                    ref={this.ref}
                    name={this.props.name}
                    required
                    style={{ display: "none" }} // or type="hidden", whatever
                    value={selected ? selected.id : ""}
                    onChange={() => {}}
                />

                ...

            </div>
        );
    }

    ...
}

And your Form component with the following:

class Form extends React.Component {

    handleChange = event => {
        console.log("Form onChange");

        // remove synthetic event from pool
        event.persist();
    };

    ...
}
Rivi answered 21/4, 2019 at 23:37 Comment(1)
Oh my god, you are the soul saver! Thank you, this works)Armbruster
F
3

I really think the best to do what you try to do is first make sure to control each individual input. Keep those values in state and just working with the onSubmit event from the form. React even recommended this approach here https://reactjs.org/docs/uncontrolled-components.html

In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.

You can read about controlled here https://reactjs.org/docs/forms.html#controlled-components

If you want to see how I will have made with just control this will have been looks like that https://codesandbox.io/s/2w9qnk8lxp You can see if you click enter the form submit event with the value keep in state.

class CustomSelect extends React.Component {
  items = [
    { id: 1, text: "Kappa 1" },
    { id: 2, text: "Kappa 2" },
    { id: 3, text: "Kappa 3" }
  ];

  render() {
    return (
      <div className="custom-select">
        <div>
          Selected: {this.props.selected ? this.props.selected.text : "nothing"}
        </div>
        {this.items.map(item => {
          return (
            <button
              key={item.id}
              type="button"
              onClick={() => this.props.onChange(item)}
            >
              {item.text}
            </button>
          );
        })}
      </div>
    );
  }
}

class Form extends React.Component {
  state = {
    firstInput: "",
    selected: null
  };

  handleSubmit = event => {
    event.preventDefault();
    console.log("Form submit", this.state);
  };

  handleInputChange = name => event => {
    this.setState({ [name]: event.target.value });
  };

  handleSelectedChanged = selected => {
    this.setState({ selected });
  };

  render() {
    console.log(this.state);
    return (
      <form onSubmit={this.handleSubmit}>
        <label>This input will trigger form's onChange event</label>
        <input
          value={this.state.firstInput}
          onChange={this.handleInputChange("firstInput")}
        />
        <CustomSelect
          name="kappa"
          selected={this.state.selected}
          onChange={this.handleSelectedChanged}
        />
      </form>
    );
  }
}

But if you really want your way, you should pass down the handleChange function as a callback to the children and make use of this props as a function when you click on an element. Example here https://codesandbox.io/s/0o8545mn1p.

class CustomSelect extends React.Component {
  items = [
    { id: 1, text: "Kappa 1" },
    { id: 2, text: "Kappa 2" },
    { id: 3, text: "Kappa 3" }
  ];

  state = {
    selected: null
  };

  handleSelect = item => {
    this.setState({ selected: item });

    this.props.onChange({ selected: item });
  };

  render() {
    var { selected } = this.state;
    return (
      <div className="custom-select">
        <input
          name={this.props.name}
          required
          style={{ display: "none" }} // or type="hidden", whatever
          value={selected ? selected.id : ""}
          onChange={() => {}}
        />
        <div>Selected: {selected ? selected.text : "nothing"}</div>
        {this.items.map(item => {
          return (
            <button
              key={item.id}
              type="button"
              onClick={() => this.handleSelect(item)}
            >
              {item.text}
            </button>
          );
        })}
      </div>
    );
  }
}

class Form extends React.Component {
  handleChange = event => {
    console.log("Form onChange");
  };

  render() {
    return (
      <form onChange={this.handleChange}>
        <label>This input will trigger form's onChange event</label>
        <input />
        <CustomSelect name="kappa" onChange={this.handleChange} />
      </form>
    );
  }
}
Fountainhead answered 15/4, 2019 at 12:15 Comment(3)
Thank you for your answer. Basically, the input that is the case of this problem is controlled, because it's value depends on the state. And I'm very sorry for my mistake, I forgot to include in use-case of my problem one very important thing - inputs are being passed to the form through children, and this kills the approach of passing the hangleChange method down to inputs.Armbruster
I have edited the question with correct use-case snippet. Sorry for wasting you time(Armbruster
@LevitatorImbalance no problem :)Fountainhead
S
3

If you pass down the function from the form you can trigger it manually. You just need to create the new Event() to suite you needs of info. Since its a prop it will sync if any method changes happen in the parent element.

Since you use props to generate elements within the form you must map them like so. This was the event only gets added to the custom elements.

class CustomSelect extends React.Component {
  propTypes: {
        onChange: React.PropTypes.func
    }
  items = [
    { id: 1, text: "Kappa 1" },
    { id: 2, text: "Kappa 2" },
    { id: 3, text: "Kappa 3" }
  ]
  
  state = {
    selected: null,
  }
  
  handleSelect = (item) => {
    this.setState({ selected: item });
    this.props.onChange.self(new Event('onchange'))
  };
  
  render() {
    var { selected } = this.state
    return (
      <div className="custom-select">
        <input
          name={this.props.name}
          required
          style={{ display: "none" }} // or type="hidden", whatever
          value={selected
            ? selected.id
            : ""
          }
          onChange={() => {}}
        />
        <div>Selected: {selected ? selected.text : "nothing"}</div>
        {this.items.map(item => {
          return (
            <button 
              key={item.id}
              type="button" 
              onClick={() => this.handleSelect(item)}
            >
              {item.text}
            </button>
          )
        })}
      </div>
    )
  }
}

class Form extends React.Component {
  handleChange = (event) => {
    console.log("Form onChange")
  }
  
  render() {
    let self = this.handleChange;
    let children = React.Children.map(this.props.children, (child, i) => {
          if(typeof child.type === "function"){
            return React.cloneElement(child, {
              onChange: {self}
            });
          }
          return child;
        });
    return (
      <form onChange={this.handleChange}>
        {children}
      </form>
    )
  }
}

ReactDOM.render(
  <Form>
    <label>This input will trigger form's onChange event</label>
    <input />
    <CustomSelect name="kappa" />
  </Form>,
  document.getElementById("__root")
 )
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>


<div id="__root"></div>
Supersonics answered 15/4, 2019 at 12:51 Comment(3)
I'm sorry, I have skipped one important thing in my use case - inputs are being passed to Form through children. I have edited my question. Sorry for wasting your time :(Armbruster
@LevitatorImbalance see changeSupersonics
Thank you. Unfortunately, this will work only if inputs are on first-level of the components tree.Armbruster
R
3

Update your CustomSelect component with the following:

class CustomSelect extends React.Component {

    ...

    // you'll use this reference to access the html input.
    ref = React.createRef();

    handleSelect = item => {
        this.setState({ selected: item });

        // React overrides input value setter, but you can call the
        // function directly on the input as context
        const inputValSetter = Object.getOwnPropertyDescriptor(
            window.HTMLInputElement.prototype,
            "value"
        ).set;
        inputValSetter.call(this.ref.current, "dummy");

        // fire event
        const ev = new Event("input", { bubbles: true });
        this.ref.current.dispatchEvent(ev);
    };

    ...

    render() {
        ...

        return (
            <div className="custom-select">
                <input
                    // you'll use the reference in `handleSelect`
                    ref={this.ref}
                    name={this.props.name}
                    required
                    style={{ display: "none" }} // or type="hidden", whatever
                    value={selected ? selected.id : ""}
                    onChange={() => {}}
                />

                ...

            </div>
        );
    }

    ...
}

And your Form component with the following:

class Form extends React.Component {

    handleChange = event => {
        console.log("Form onChange");

        // remove synthetic event from pool
        event.persist();
    };

    ...
}
Rivi answered 21/4, 2019 at 23:37 Comment(1)
Oh my god, you are the soul saver! Thank you, this works)Armbruster

© 2022 - 2024 — McMap. All rights reserved.