State management issue with react ul contenteditable

82 Views Asked by At

I'm trying to create a custom form field that displays and edits string array as an editable ul list - the field creates a bullet for every string item in the array.

list field

The issue is when I try to manage the field's state with a value and onChange props (like a regular form field) - the field duplicates the values that are being displayed and as a result, it's also set the value with the duplicates.

This is a piece of code that reproduces the issue:

import { useMemo, useState, type FormEvent } from 'react';

export default function List() {
  const [value, setValue] = useState<string[]>(['first line', 'second line']);
  const listItems = useMemo(
    () => Array.from(Array.isArray(value) ? value : [value]),
    [value]
  );

  function handleInput(event: FormEvent) {
    const items = event.currentTarget.innerHTML
      .split(/<li>(.*?)<\/li>/)
      ?.map((item: string) => item.replace(/<(.*?)>/g, ''))
      ?.filter(Boolean);
    setValue(items);
  }

  return (
    <ul
      contentEditable
      suppressContentEditableWarning
      onInput={handleInput}
    >
      {listItems.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

I use a local state to demonstrate the issue but in reality, the value and onChange will be injected as props

Some conclusions I've come to while playing with this:

  1. There is a react inner state that manages the ul element inner HTML perfectly. A temporary solution I use is to call onChange on the ul's onBlur event.
  2. When I use dangerouslySetInnerHTML and set the li as strings - The cursor jumps to the beginning of the list for every stroke and I can't set the cursor position because I don't have a reference to the current edited line - but that's an issue or another question

Thanks

1

There are 1 best solutions below

1
Anshu On

use react-contenteditable

The EditableListField component takes in two props: value and onChange. value is an array of strings that represent the list items, and onChange is a function that updates the list items. The EditableListField component maintains a state variable html that stores the HTML representation of the list. When the value prop changes, the useEffect hook updates html to reflect the new list items. The handleChange function is triggered when the user edits the list. It parses the updated HTML, extracts the list items, and calls the onChange prop with the new list items.

The App component maintains a state variable listItems that stores the current list items. It renders the EditableListField component and passes listItems and a setter function setListItems as props.

import "./styles.css";

import React, { useState, useEffect, ChangeEvent } from "react";
import ContentEditable from "react-contenteditable";

interface EditableListFieldProps {
  value: string[];
  onChange: (newItems: string[]) => void;
}

const EditableListField: React.FC<EditableListFieldProps> = ({ value, onChange }) => {
  const [html, setHtml] = useState<string>("");

  useEffect(() => {
    const listHtml = value.map((item) => `<li>${item}</li>`).join("");
    setHtml(`<ul>${listHtml}</ul>`);
  }, [value]);

  const handleChange = (event: ChangeEvent<HTMLDivElement>) => {
    const parser = new DOMParser();
    const doc = parser.parseFromString(event.currentTarget.innerHTML, "text/html");
    const items = Array.from(doc.querySelectorAll("li")).map(
      (li) => li.textContent || ""
    );
  
    onChange(items);
    setHtml(event.currentTarget.innerHTML);
  };
  

  return <ContentEditable html={html} onChange={handleChange} tagName="div" />;
};

const App: React.FC = () => {
  const [listItems, setListItems] = useState<string[]>(["Item 1", "Item 2", "Item 3"]);

  const handleListChange = (newItems: string[]) => {
    setListItems(newItems);
  };

  return (
    <div>
      <label>Editable List:</label>
      <EditableListField value={listItems} onChange={handleListChange} />
    </div>
  );
};

export default App;

If you wanna play around with the code