skip to Main Content

I have a Textarea component that handles Markdown headers:

type TextareaProps = {
    initValue: string;
    style?: StyleProp<TextStyle>;
    onChange?: (value: string) => void;
};

type OnChangeFun = NativeSyntheticEvent<TextInputChangeEventData>;

const Textarea = ({initValue, style, onChange = () => {}}: TextareaProps) => {
  const [value, setValue] = useState<string>(initValue);

  const changeHandler = ({nativeEvent: {text}}: OnChangeFun) => {
    setValue(text);
    onChange(text);
  };
  return (
    <TextInput
      style={[styles.textarea, style]}
      multiline
      onChange={changeHandler}>
      <Text>
        {value.split('n').map((line, index) => {
          const style = line.match(/^#/) && styles.header;
          return (
              <Fragment key={`${index}-${line}`}>
                <Text style={style} >{ line }</Text>
                {"n"}
              </Fragment>
          );
        })}
      </Text>
    </TextInput>
  );
};

The problem is that when I enter a character the cursor jumps two characters. If it’s the last character in the line it jumps to the next line.

I’ve tried to add controlled selection:

const Textarea = ({initValue, style, onChange = () => {}}: TextareaProps) => {
  const [value, setValue] = useState<string>(initValue);
  const [selection, setSelection] = useState<Selection>({
    start: 0,
    end: 0
  });
  useEffect(() => {
    setSelection(({start, end}) => {
      if (start === end) {
        start += 1;
        end = start;
      }
      return { start, end };
    });
  }, [value]);
  const changeHandler = ({nativeEvent: {text}}: OnChangeFun) => {
    setValue(text);
    onChange(text);
  };
  const onSelection = ({ nativeEvent: { selection }}: OnSelectionFun) => {
    setSelection(selection);
  };
  return (
    <TextInput
      selection={selection}
      style={[styles.textarea, style]}
      multiline
      onSelectionChange={onSelection}
      onChange={changeHandler}>
      <Text>
        {value.split('n').map((line, index) => {
          const style = line.match(/^#/) && styles.header;
          return (
            <Fragment key={`${index}-${line}`}>
              <Text style={style} >{ line }</Text>
              <Text>{"n"}</Text>
            </Fragment>
          );
        })}
      </Text>
    </TextInput>
  );
};

But this makes the whole content disappear when I type something or click inside the input.

Is there a way to add a new line after each line in the Rich Text editor and have the cursor in the right position?

Unfortunately, I can’t create Snack with the code because this is totally broken in Snack, the output of the Textarea is [object Object].

2

Answers


  1. Chosen as BEST ANSWER

    I was looking into all questions at: How can I insert a line break into a component in React Native? and found an answer by Charlie Morton based his idea I came up with this code:

    const Textarea = ({ initValue, style, onChange = () => {} }: TextareaProps) => {
      const [value, setValue] = useState<string>(initValue);
      const changeHandler = ({ nativeEvent: { text } }: OnChangeFun) => {
        setValue(text);
        onChange(text);
      };
      return (
        <TextInput
          selection={selection}
          style={[styles.textarea, style]}
          multiline
          onSelectionChange={onSelection}
          onChange={changeHandler}>
          <Text>
            {value.split(/(n)/).map((line, index) => {
              const key = `${index}-${line}`;
              if (line === 'n') {
                return <Text key={key}>{ line }</Text>;
              }
              const style = line.match(/^#/) && styles.header;
              return <Text key={key style={style}>{ line }</Text>;
            })}
          </Text>
        </TextInput>
      );
    };
    

    When newline is its own element own item in the array, the text input magically works.


  2. The problem in your original code is caused by adding extra n after the last line. Think about it: say you have value with 3 lines. When you render it inside TextInput, your next value will have 4 lines:

    line1n
    line2n
    line3n
    (empty line 4)
    

    If you replace {"n"} with conditional render, the problem is solved:

    const lines = value.split('n');
    // ...
    return (
      <Fragment key={`${index}-${line}`}>
        <Text style={textStyle}>{line}</Text>
        {lines.length === index + 1 ? null : 'n'}
      </Fragment>
    );
    

    P. S. The cursor is moving by 1 extra character because apparently React Native tracks cursor position relative to the end of the input value; since you added 1 extra character to the end of the value, cursor remained fixed to N characters from the end, which caused it to visually jump by 1 extra character to the right with every update.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search