Jay Pondy
Jay Pondy

Reputation: 176

Passing Props to Child component from Parent using cloneElement

In the code below when the checkbox is checked in AddressWrapper the Ship To input in the AddressForm should be disabled. I can not figure out why AddressWrapper cloneElement is not passing it's state to the child. I have checked out many links about this issue and as far as I can tell this should work. This is the closest How to pass props to {this.props.children} to this problem but it is using a callback from the child to the parent and I need a change in parent state to update the child. I could use a publish/subscribe to do it but I'm trying to do it the 'React' way.

class AddressForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: "Joyce",
      disableInputs: props.billToSameAsShipTo
    };
    this.handleBillToSameAsShipToChanged = this.handleBillToSameAsShipToChanged.bind(
      this
    );
  }

  handleBillToSameAsShipToChanged() {
    this.setState({ billToSameAsShipTo: !this.state.billToSameAsShipTo });
  }

  handleFirstNameChanged(ev) {
    this.setState({ firstName: ev.target.value });
  }

  render() {
    return (
      <form>
        <div className="form-row">
          <div className="col-6">
            <input
              type="text"
              className="form-control"
              placeholder="First name"
              disabled={this.state.disableInputs}
              value={this.state.firstName}
              onChange={this.handleFirstNameChanged.bind(this)}
            />
          </div>
        </div>
      </form>
    );
  }
}

class AddressFormWrapper extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      billToSameAsShipTo: true
    };
    this.handlebillToSameAsShipToChanged = this.handlebillToSameAsShipToChanged.bind(
      this
    );
  }

  handlebillToSameAsShipToChanged() {
    this.setState({ billToSameAsShipTo: !this.state.billToSameAsShipTo });
  }

  render() {
    const billToSameAsShipTo = () => {
      if (this.props.showSameAsShipTo === true) {
        return (
          <span style={{ fontSize: "10pt", marginLeft: "20px" }}>
            <input
              type="checkbox"
              checked={this.state.billToSameAsShipTo}
              onChange={this.handlebillToSameAsShipToChanged}
            />
            &nbsp;
            <span>Same as Ship To</span>
          </span>
        );
      }
    };

    const childWithProp = React.Children.map(this.props.children, child => {
      return React.cloneElement(child, { ...this.state });
    });

    return (
      <span className="col-6">
        <h3>
          {this.props.title}
          {billToSameAsShipTo()}
        </h3>
        <span>{childWithProp}</span>
      </span>
    );
  }
}

const Checkout = () => {
  return (
    <div>
      <br />
      <br />
      <div className="row">
        <AddressFormWrapper title="Ship To" showSameAsShipTo={false}>
          <span className="col-6">
            <AddressForm />
          </span>
        </AddressFormWrapper>

        <AddressFormWrapper title="Bill To" showSameAsShipTo={true}>
          <span className="col-6">
            <AddressForm />
          </span>
        </AddressFormWrapper>
      </div>
    </div>
  );
};

Upvotes: 1

Views: 75

Answers (1)

Sagiv b.g
Sagiv b.g

Reputation: 31024

In AddressFormWrapper you map over the children and passing props with cloneElement().

As per the DOCS:

Invokes a function on every immediate child contained within children...

But take a good look who are those (immediate) children of AddressFormWrapper:

<AddressFormWrapper title="Bill To" showSameAsShipTo={true}>
  <span className="col-6">
    <AddressForm />
  </span>
</AddressFormWrapper>

In this case its the span element and not AddressForm.

If you render it like this it will work as expected:

<AddressFormWrapper title="Bill To" showSameAsShipTo={true}>
    <AddressForm />
</AddressFormWrapper>

Another thing to watch out from, in AddressForm you are setting the state:

disableInputs: props.billToSameAsShipTo

This is inside the constructor and it will only run once. So it will get the initial value but won't get changed.
Either update it in componentDidUpdate or better just use the props directly:

disabled={this.props.billToSameAsShipTo}

Here is a running example:

class AddressForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      firstName: "Joyce",
      disableInputs: props.billToSameAsShipTo
    };
    this.handleBillToSameAsShipToChanged = this.handleBillToSameAsShipToChanged.bind(
      this
    );
  }

  handleBillToSameAsShipToChanged() {
    this.setState({ billToSameAsShipTo: !this.state.billToSameAsShipTo });
  }

  handleFirstNameChanged(ev) {
    this.setState({ firstName: ev.target.value });
  }

  billToSameAsShipTo() {
    if (this.props.showSameAsShipTo === true) {
      return (
        <span style={{ fontSize: "10pt" }}>
          <input
            type="checkbox"
            checked={this.state.billToSameAsShipTo}
            onChange={this.handleBillToSameAsShipToChanged}
          />&nbsp;<span>Same as Ship To</span>
        </span>
      );
    }
  }

  render() {
    return (
      <form>
        <div className="form-row">
          <div className="col-6">
            <input
              type="text"
              className="form-control"
              placeholder="First name"
              disabled={this.props.billToSameAsShipTo}
              value={this.state.firstName}
              onChange={this.handleFirstNameChanged.bind(this)}
            />
          </div>
        </div>
      </form>
    );
  }
}

class AddressFormWrapper extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      billToSameAsShipTo: true
    };
    this.handlebillToSameAsShipToChanged = this.handlebillToSameAsShipToChanged.bind(
      this
    );
  }

  handlebillToSameAsShipToChanged() {
    this.setState({ billToSameAsShipTo: !this.state.billToSameAsShipTo });
  }

  render() {
    const billToSameAsShipTo = () => {
      if (this.props.showSameAsShipTo === true) {
        return (
          <span style={{ fontSize: "10pt", marginLeft: "20px" }}>
            <input
              type="checkbox"
              checked={this.state.billToSameAsShipTo}
              onChange={this.handlebillToSameAsShipToChanged}
            />&nbsp;<span>Same as Ship To</span>
          </span>
        );
      }
    };

    const childWithProp = React.Children.map(this.props.children, child => {
      return React.cloneElement(child, { ...this.state });
    });

    return (
      <span className="col-6">
        <h3>
          {this.props.title}
          {billToSameAsShipTo()}
        </h3>
        <span>{childWithProp}</span>
      </span>
    );
  }
}

const Checkout = () => {
  return (
    <div>
      <br />
      <br />
      <div className="row">
        <AddressFormWrapper title="Ship To" showSameAsShipTo={false}>
          <span className="col-6">
            <AddressForm />
          </span>
        </AddressFormWrapper>

        <AddressFormWrapper title="Bill To" showSameAsShipTo={true}>
          <AddressForm />
        </AddressFormWrapper>
      </div>
    </div>
  );
};

ReactDOM.render(<Checkout />, document.querySelector("#app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"/>

Upvotes: 1

Related Questions