ControlValueAccessor - string concatenation

I’ve started playing around with implementing a custom form control for Angular. See https://github.com/jackwootton/codemirror.

My implementation (cm.component.ts) implements the ControlValueAccessor interface (among others). As part of implementing this interface, my Angular component must call a function when the Codemirror view is updated. Here’s how I’m doing that right now:

const changeHandler: Extension = EditorView.updateListener.of((v: ViewUpdate) => {
   if (v.docChanged) {
     const s = this.cm.state.doc.toString();
     this._onChange(s); // update the Angular control
   }
});

new EditorView({
    parent: this.host?.nativeElement,
    state: EditorState.create({
        extensions: [basicSetup, changeHandler],
    }),
});

My concern is that in CodeMirror 6: Proper way to listen for changes, it is mentioned that

Though for giant documents it’s going to do quite a lot of string concatenation, so depending on your use case you might not want to constantly do it when you can avoid it.

This means that for every change, my component is triggering a lot of string concatenation so that it can update the Angular Form Control.

Is there a way I can get the latest content of the editor without doing all this string concatenation?

Not really—the editor doesn’t keep its document as a plain string (that would be expensive for huge documents, which is a use case the library supports). So if you really need the value as a string, you’ll have to live with the fact that it’ll get slower for large documents. But profile this—maybe it doesn’t matter for any doc size that you’ll reasonably work with.

I dealt with a similar issue, with a Svelte component that needed to interact with other components. I can give three ways to approach this:

  1. Make retrieving the editor’s content a memoized getter/function, so that this expensive operation only happens when it’s asked for
  2. Make the retrieval function asynchronous, and use the fact that the document (a Text object) can be iterated, e.g. for (const str of state.doc) or something similar. You can then throttle your own concatenation to prevent the main thread from stuttering, if that’s the concern.
  3. Just expose the Text object directly - it has a nice interface, and the consumer can just toString() it if they really need to.

If none of these are practical for you, that’s unfortunate because, ultimately, there is no performant way to get the entire document as a big string synchronously.

I like this idea. One wrinkle is that the control value accessor component’s get and set methods must be the same. This means the user of the component must provide a Text instance to use the control. Does Text have a nice constructor that accepts a string, so any a user of the component can easily provide an initial value?

let firstValue = Text("this will be the initial value, set by the client");

Text doesn’t provide a method/constructor that is as clean as your example, but you can do:

let firstValue = Text.of(["this will be the initial value, set by the client"])

Each string in the array is a line.

It would be interesting to have a “cleaner”, more automatic static method for creating a Text object, but the issue that Text.of was avoiding would crop up, which is line separators. Would still be nice to have an “automagic” one, and the @codemirror/text package needs an update anyways because it still hasn’t gotten the updated .d.ts comments lol

That works. Although it does cause new lines to be rendered as NL symbols.

It looks like EditorState.doc can be initialized to a Text instance, so I think the rendering of special characters in this way, is a bug?