Alon Burg
Alon Burg

Reputation: 2550

How to inject/pass attributes to nested elements

I have the following component hierarchy:

<Form>
  <div>
     <Label>
      <Input name="first_name" />
     </Label>
  </div>
  <Label>
    <Input name="first_name" />
  </Label>
</Form

I would like to implement the following behaviors:

  1. All <Form> components should implement a behavior that will autofocus the first <Input>, without the <Input> having to specify this each time again and again. Using the autofocus manually in every <Form> is prone to error, and developers tend to forget it. As for now, I've decided using a code like this inside <Form> component:

    componentDidMount() {
      $(ReactDOM.findDOMNode(this)).find('input:first:visible').focus()
    }
    
  2. <Label> elements should have a for/htmlFor + <Input> attribute that matches the <Input> id inside the <Label> without the developer having to specify it manually each time. I am considering using a recursive cloneElement and injecting the for and id attributes but this sounds too cumbersome and not elegant.

Any ideas?

Upvotes: 1

Views: 355

Answers (1)

Calvin Belden
Calvin Belden

Reputation: 3114

So I think we can accomplish what you're looking for by creating a few custom components: Form and FormGroup.

Form will be responsible for setting a prop on the FormGroup specifying whether or not it should be focused (I'm assuming that all of Form's children are FormGroup instances):

class Form extends React.Component {
  render() {
    const children = React.Children.map(this.props.children, (el, i) => {
      const focused = i === 0;
      return React.cloneElement(el, { focused });
    });

    return <form>{children}</form>;
  }
}

And our FormGroup component has a few responsibilities.

  1. It needs to focus the input element when appropriate
  2. It needs to automatically set the htmlFor attribute for the label element

This code is a bit more involved:

class FormGroup extends React.Component {
  render() {
    let input = null;
    let label = null;

    // Get references to the input and label elements
    React.Children.forEach(this.props.children, el => {
      switch (el.type) {
        case 'input':
          input = el;
          return;
        case 'label':
          label = el;
          return;
      }
    });

    if (input === null || label === null) {
       throw new Error('FormGroup must be used with and input and label element');
    }

    // Augment: add the htmlFor and autoFocus attributes
    label = React.cloneElement(label, { htmlFor: input.props.id });
    input = React.cloneElement(input, { autoFocus: this.props.focused });

    // Render our augmented instances
    return <div>{label}{input}</div>;
  }
}

Now that we have our building blocks, we can create forms with the desired behavior:

<Form>
  <FormGroup>
    <label>First Label</label>
    <input id="first" type="text" />
  </FormGroup>
  <FormGroup>
    <label>Second Label</label>
    <input id="second" type="text" />
  </FormGroup>
</Form>

For this form, the #first input would be focused and each label element would have the correct for attributes.

Hopefully this will get you on the right track. Here's a webpackbin of this setup: http://www.webpackbin.com/VJVY1a7Tg

Upvotes: 1

Related Questions