12

I'm trying to focus/highlight input text onClick in React. It works as expected, but only on the last element in the rendered array. I've tried several different methods but they all do the exact same thing. Here are two examples of what I have:

export default class Services extends Component {

handleFocus(event) {
    event.target.select()
}

handleClick() {
    this.textInput.focus()
}


render() {
    return (
        <div>
            {element.sources.map((el, i) => (
                <List.Item key={i}>
                <Segment style={{marginTop: '0.5em', marginBottom: '0.5em'}}>
                    <Input fluid type='text'
                        onFocus={this.handleFocus}
                        ref={(input) => { this.textInput = input }} 
                        value='text to copy'
                        action={
                            <Button inverted color='blue' icon='copy' onClick={() => this.handleClick}></Button>
                        }
                    />
                </Segment>
                </List.Item>
            ))}
        </div>
    )
}

If there's only one element being rendered, it focuses the text in the input, but if there are multiple elements, every element's button click selects only the last element's input. Here's another example:

export default class Services extends Component {

constructor(props) {
    super(props)

    this._nodes = new Map()
    this._handleClick = this.handleClick.bind(this)
}

handleFocus(event) {
    event.target.select()
}

handleClick(e, i) {
    const node = this._nodes.get(i)
    node.focus()
}


render() {
    return (
        <div>
            {element.sources.map((el, i) => (
                <List.Item key={i}>
                <Segment style={{marginTop: '0.5em', marginBottom: '0.5em'}}>
                    <Input fluid type='text'
                        onFocus={this.handleFocus}
                        ref={c => this._nodes.set(i, c)} 
                        value='text to copy'
                        action={
                            <Button inverted color='blue' icon='copy' onClick={e => this.handleClick(e, i)}></Button>
                        }
                    />
                </Segment>
                </List.Item>
            ))}
        </div>
    )
}

Both of these methods basically respond the same way. I need the handleClick input focus to work for every dynamically rendered element. Any advice is greatly appreciated. Thanks in advance!

The Input component is imported from Semantic UI React with no additional implementations in my app

UPDATE Thanks guys for the great answers. Both methods work great in a single loop element render, but now I'm trying to implement it with multiple parent elements. For example:

import React, { Component } from 'react'
import { Button, List, Card, Input, Segment } from 'semantic-ui-react'

export default class ServiceCard extends Component {

handleFocus(event) {
    event.target.select()
}

handleClick = (id) => (e) => {
    this[`textInput${id}`].focus()
}

render() {
    return (
        <List divided verticalAlign='middle'>
            {this.props.services.map((element, index) => (
                <Card fluid key={index}>
                    <Card.Content>
                        <div>
                            {element.sources.map((el, i) => (
                                <List.Item key={i}>
                                    <Segment>
                                        <Input fluid type='text'
                                            onFocus={this.handleFocus}
                                            ref={input => { this[`textInput${i}`] = input }} 
                                            value='text to copy'
                                            action={
                                                <Button onClick={this.handleClick(i)}></Button>
                                            }
                                        />
                                    </Segment>
                                </List.Item>
                            ))}
                        </div>
                    </Card.Content>
                </Card>
            ))}
        </List>
    )
}

Now, in the modified code, your methods work great for one Card element, but when there are multiple Card elements, it still only works for the last one. Both Input Buttons work for their inputs respectively, but only on the last Card element rendered.

merrilj
  • 327
  • 1
  • 2
  • 10
  • Possible duplicate of [React Select mapping issue](https://stackoverflow.com/questions/46467577/react-select-mapping-issue) – bennygenel Sep 28 '17 at 16:55
  • It's different in the fact that other methods on the input work fine for every element besides handleClick. The ref only selects the last element rendered and no others. – merrilj Sep 28 '17 at 17:02
  • @MerrilJeffs The second code will work as expected. Are you getting any error on console for second code? – Prakash Sharma Sep 28 '17 at 17:27
  • show the code for your `Input` component to see how you implement the action (`Button`) events – Sagiv b.g Sep 28 '17 at 18:45
  • @Prakashsharma I am not getting any errors. It still only focuses the last input element in the array regardless of which element's button is clicked. – merrilj Sep 28 '17 at 18:47
  • @Sag1v Input is just an imported Semantic UI React component with no additional implementations on my end. – merrilj Sep 28 '17 at 18:53
  • got you, its a good idea to mention it in your question and add a relevant tag. as i understand the focus is working but the button click event is not. how does the relation of the buttons and inputs works? the library binds them together? – Sagiv b.g Sep 28 '17 at 19:03
  • 1
    @MerrilJeffs Second code is working. Here is the working example https://codesandbox.io/s/p3y90wmp7m – Prakash Sharma Sep 28 '17 at 19:12
  • i found your problem, you have a list of refs but you overriding the same one. i'm posting the answer with a working code – Sagiv b.g Sep 28 '17 at 19:14
  • Yes, precisely. And both the input and button are Semantic UI components. If I pass in the element index it understands each respective element, but only performs the textInput focus on the last element in the array. – merrilj Sep 28 '17 at 19:15

1 Answers1

11

You are setting a ref inside a loop, as you already know, the ref is set to the class via the this key word. This means that you are setting multiple refs but overriding the same one inside the class.
One solution (not the ideal solution) is to name them differently, maybe add the key to each ref name:

        ref={input => {
          this[`textInput${i}`] = input;
        }}

and when you target that onClick event of the Button you should use the same key as a parameter:

 action={
                  <Button
                    inverted
                    color="blue"
                    icon="copy"
                    onClick={this.handleClick(i)}
                  >
                    Focus
                  </Button>
                }

Now, the click event should change and accept the id as a parameter and trigger the relevant ref (i'm using currying here):

  handleClick = (id) => (e) => {
      this[`textInput${id}`].focus();
  }

Note that this is and easier solution but not the ideal solution, as we create a new instance of a function on each render, hence we pass a new prop which can interrupt the diffing algorithm of react (a better and more "react'ish" way coming next).

Pros:

  • Easier to implement
  • Faster to implement

Cons:

  • May cause performance issues
  • Less the react components way

Working example

This is the full Code:

class Services extends React.Component {

  handleFocus(event) {
    event.target.select();
  }


  handleClick = id => e => {
    this[`textInput${id}`].focus();
  };

  render() {
    return (
      <div>
        {sources.map((el, i) => (
          <List.Item key={i}>
            <Segment style={{ marginTop: "0.5em", marginBottom: "0.5em" }}>
              <Input
                fluid
                type="text"
                onFocus={this.handleFocus}
                ref={input => {
                  this[`textInput${i}`] = input;
                }}
                value="text to copy"
                action={
                  <Button
                    inverted
                    color="blue"
                    icon="copy"
                    onClick={this.handleClick(i)}
                  >
                    Focus
                  </Button>
                }
              />
            </Segment>
          </List.Item>
        ))}
      </div>
    );
  }
}

render(<Services />, document.getElementById("root"));

A better and more "react'ish" solution would be to use component composition or a HOC that wraps the Button and inject some simple logic, like pass the id instead of using 2 functions in the parent.

Pros:

  • As mentioned, Less chances of performance issues
  • You can reuse this component and logic
  • Sometimes easier to debug

Cons:

  • More code writing

  • Another component to maintain / test etc..

A working example
The full Code:

class MyButton extends React.Component {

  handleClick = (e) =>  {
    this.props.onClick(this.props.id)
  }

  render() {
    return (
      <Button
      {...this.props}
        onClick={this.handleClick}
      >
        {this.props.children}
      </Button>
    )
  }
}


class Services extends React.Component {

  constructor(props){
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleFocus(event) {
    event.target.select();
  }


  handleClick(id){
    this[`textInput${id}`].focus();
  };

  render() {
    return (
      <div>
        {sources.map((el, i) => (
          <List.Item key={i}>
            <Segment style={{ marginTop: "0.5em", marginBottom: "0.5em" }}>
              <Input
                fluid
                type="text"
                onFocus={this.handleFocus}
                ref={input => {
                  this[`textInput${i}`] = input;
                }}
                value="text to copy"
                action={
                  <MyButton
                    inverted
                    color="blue"
                    icon="copy"
                    onClick={this.handleClick}
                    id={i}
                  >
                    Focus
                  </MyButton>
                }
              />
            </Segment>
          </List.Item>
        ))}
      </div>
    );
  }
}

render(<Services />, document.getElementById("root"));

Edit
As a followup to your edit:

but when there are multiple Card elements, it still only works for the last one.

This happens for the same reason as before, you are using the same i for both arrays.
This is an easy solution, use both index and i for your ref names.
Setting the ref name:

ref={input => { this[`textInput${index}${i}`] = input }}

Passing the name to the handler:

<Button onClick={this.handleClick(`${index}${i}`)}></Button>

Working example

I've modified my question and provided a second solution that is considered best practice. read my answer again and see the different approaches.

Sagiv b.g
  • 26,049
  • 8
  • 51
  • 86