0

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;
    }
`;
3
  • Can you share your TextArea code? Commented Oct 18, 2020 at 14:40
  • The problem is document.querySelector('textarea').addEventListener('input', (e) => { caret = getCaretCoordinates(e.target, e.target.selectionEnd); this.setState({ caret }); }); Commented Oct 18, 2020 at 14:50
  • What should I do? @Domino987 Commented Oct 18, 2020 at 14:54

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.