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.
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:
Cons:
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:
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>
I've modified my question and provided a second solution that is considered best practice. read my answer again and see the different approaches.