Reputation: 35
Here is my class component which is a twitter like auto-suggest text area.
import React, { Component, createRef } from 'react';
import { TextArea, List, Triangle } from './Styled';
import getCaretCoordinates from 'textarea-caret';
class AutoCompleteTextArea extends Component {
constructor(props) {
super(props);
this.state = {
value: "",
caret: {
top: 0,
left: 0,
},
isOpen: false,
matchType: null,
match: null,
list: [],
selectionEnd: null,
};
this.users = [
{ name: 'Bob', id: 'bobrm' },
{ name: 'Andrew', id: 'andrew_s' },
{ name: 'Simon', id: 'simon__a' },
];
this.hashtagPattern = new RegExp(`([#])(?:(?!\\1)[^\\s])*$`);
this.userPattern = new RegExp(`([@])(?:(?!\\1)[^\\s])*$`);
this.textareaRef = createRef();
}
componentDidMount() {
var caret;
document.querySelector('textarea').addEventListener('input', (e) => {
caret = getCaretCoordinates(e.target, e.target.selectionEnd);
this.setState({ caret });
});
}
keyDown = (e) => {
this.autosize();
const code = e.keyCode || e.which;
// Down
//if (code === 40) down()
// Up
//if (code === 38) up()
// Enter
//if (code === 13) onSelect()
};
onChange = (e) => {
const { selectionEnd, value } = e.target;
console.log(value);
this.setState({ value });
const userMatch = this.userPattern.exec(value.slice(0, selectionEnd));
const hashtagMatch = this.hashtagPattern.exec(
value.slice(0, selectionEnd)
);
if (hashtagMatch && hashtagMatch[0]) {
this.setState({
matchType: hashtagMatch[1],
match: hashtagMatch[0],
selectionEnd,
});
this.suggest(hashtagMatch[0], hashtagMatch[1]);
} else if (userMatch && userMatch[0]) {
this.setState({
matchType: userMatch[1],
match: userMatch[0],
selectionEnd,
});
this.suggest(userMatch[0], userMatch[1]);
} else {
this.setState({
match: null,
matchType: null,
isOpen: false,
});
}
};
suggest = (match, matchType) => {
if (matchType === '#') {
someRequest.then((res) => {
this.setState({
list: res.body,
isOpen: res.body.length !== 0,
});
});
} else if (matchType === '@') {
this.setState({
list: this.users,
isOpen: this.users.length !== 0,
});
}
};
autosize = () => {
var el = document.getElementsByClassName('autoComplete')[0];
setTimeout(function() {
el.style.cssText = 'height:auto; padding:0';
el.style.cssText = 'height:' + el.scrollHeight + 'px';
}, 0);
};
hashtagClickHandler = (hashtag) => {
const { selectionEnd, match, matchType, value } = this.state;
const select = matchType + hashtag;
// It's replace value text
const pre = value.substring(0, selectionEnd - match.length) + select;
const next = value.substring(selectionEnd);
const newValue = pre + next;
console.log(newValue);
this.setState({ isOpen: false, value: newValue });
this.textareaRef.current.selectionEnd = pre.length;
};
render() {
return (
<>
<TextArea
id="postText"
name="postText"
placeholder="What's on your mind ? ..."
maaxLength={255}
className="autoComplete"
onKeyDown={this.keyDown}
onChange={this.onChange}
value={this.state.value}
ref={this.textareaRef}
/>
{this.state.isOpen && (
<List top={this.state.caret.top}>
<Triangle left={this.state.caret.left} />
{this.state.matchType === '#'
? this.state.list.map((hashtag, index) => (
<button
key={'hashtag' + index}
className="listItem"
onClick={() =>
this.hashtagClickHandler(
hashtag,
index
)
}
>
<h5>{`#${hashtag}`}</h5>
</button>
))
: this.state.list.map((user, index) => (
<button
key={'user' + index}
className="listItem"
>
<h5>{user.name}</h5>
<p>{user.id}</p>
</button>
))}
</List>
)}
</>
);
}
}
export default AutoCompleteTextArea;
When I have both value and onChange props on my TextArea it doesn't fire the onChange function. But if I remove the value prop it will be fired. Actually I need the onChange because I'm going to change the value when the user clicks on one of the hashtags suggested. My TextArea is just a styled component like this:
export const TextArea = styled.textarea`
float: left;
width: 450px;
font-size: 16px;
font-weight: lighter;
margin: 22px 0 0 20px;
border: none;
&::placeholder {
color: ${(props) => props.theme.textGrayLight};
}
font-family: ${(props) => props.theme.mainFont};
position: relative;
resize: none;
@media ${(props) => props.mediaHD} {
width: 250px;
}
`;
Upvotes: 0
Views: 1561
Reputation: 8804
The problem is, that the inputs cannot have multiple input
event listener.
Since you already have a input listener(the onChange
), why do you not move the caret setting to this:
onChange = (e) => {
const { selectionEnd, value } = e.target;
const caret = getCaretCoordinates(e.target, selectionEnd);
this.setState({ caret, value });
and remove the didMount
, which overrides the listener:
componentDidMount() {
var caret;
document.querySelector('textarea').addEventListener('input', (e) => {
caret = getCaretCoordinates(e.target, e.target.selectionEnd);
this.setState({ caret });
});
}
Here is a sandbox.
Upvotes: 1