Plain Text Input Component with Y.Text

Does anyone know of a lightweight solution to handle plain text input?

I’m looking for something that accepts user from an <input>, transforms into a delta, & applies the delta to a Y.Text value. I don’t want to bring in Quill since i don’t have any rich text. I do want multiple users to edit the same field at the same time, so i don’t want to use a string. I haven’t been able to find anything. Is there a solution for plain text?

Thank you.

I’m not aware of anything, but I thought it was an interesting question so I put together a minimal demo. It has one additional dependency on fast-diff.

Demo: View in CodeSandbox

import * as Y from "yjs";
import { useState } from "react";
import diff from "fast-diff";

const doc = new Y.Doc();
const ytext = doc.getText();

/** A hook to read and set a YText value. */
function useText(ytext) {
  const [text, setText] = useState(ytext.toString());
  ytext.observe(() => {
    setText(ytext.toString());
  });
  const setYText = (textNew) => {
    const delta = diffToDelta(diff(text, textNew));
    ytext.applyDelta(delta);
  };
  return [text, setYText];
}

/** Convert a fast-diff result to a YJS delta. */
function diffToDelta(diffResult) {
  return diffResult.map(([op, value]) =>
    op === diff.INSERT
      ? { insert: value }
      : op === diff.EQUAL
      ? { retain: value.length }
      : op === diff.DELETE
      ? { delete: value.length }
      : null
  );
}

export default function App() {
  const [text, setText] = useText(ytext);
  return (
    <div>
      <input
        onInput={(e) => {
          setText(e.target.value);
        }}
      />
      <p>Text: {text}</p>
    </div>
  );
}

@raine I like your solution a lot, thank you. It’s easy to understand & lightweight.

How do you think it handles repeated strings? If i diff 'foofoo' & 'foofoofoo', how would it know if i added another foo to the beginning or the end of the string? Would it matter?

Thanks again.

Good point. fast-diff would not account for the browser selection. Presumably the algorithm moves left-to-right and thus would give a retain + insert even if the user edited the beginning.

Most edits will only occur at the beginning of the browser selection or after. In this case, you can retain everything up to the beginning of the browser selection and diff everything after. However I’m not sure if this holds for copy/paste or autocorrect (which can correct multiple words before the selection).

It might have an impact if two users are editing the same part of the text, but hard to say if it would be significant. It seems like a fairly remote edge case. If one user edits foofoo to foofoofoo, and another user edits foofoo to froofroo at the same time, I guess you would just get foofroofroo or froofroofoo. If it’s a concurrent edit then the second user technically can’t tell what the first user intended since they are both perfectly valid edits!

(Note that my demo may not perform as expected if setText is called concurrently on the same client since setYText references text through a closure. It probably should be referenced through a useState setter function, e.g. setText(textOld => ...).)