Try   HackMD

By qbane (the thread 🧵)

2022/8

Update 2023/8/18

The author has found a leaner approach: https://github.com/PuruVJ/neocodemirror.
tl;dr. It provides an action and let you optionally create a specific store for passing in. The data would be notified via the store only if a store is present.

Comment: The "use:action" syntax may be more mighty than one might have thought!

Minimal PoC: https://svelte.dev/repl/dfa4759d5bdc46a4ae4a18a6c71ade17?version=3.48.0

The "reactivity"

First thoughts:

  • A <textarea> but fancier! Get/set its state in Svelte's pattern.
  • Forward on: bound events?

We can see from the docs:

EditorStateConfig

  • doc: can be either a plain string or Text
  • selection: the initial selection, can be a {anchor, head?}
  • extensions: all other extension points; some presets are basicSetup or minimalSetup.

EditorViewConfig

Some insights of mine:

  • The state itself is immutable upon built. Once it's changed, the view must be notified at idle phase (!) to reflect the change ("setState", reconfigure, …). CodeMirror does have it's own lifecycle and updating the view wrongly will throw errors.
  • The initial selection depends on the doc. Making it reactive to doc change is convoluted. I am not going to implement it as a prop.
  • In the view config, it makes little sense to specify parent and root for making bindings for Svelte.
  • We may roll our own dispatch to pull off the reactivity. It is crucial that this is synchronous, as opposed to the one EditorView.updateListener (receives a ViewUpdate with zero or more transactions), which is asynchronous. Svelte's store contract also mandates some operations to be synchorous.

Performance in mind

Making doc bindable may harm the performance! To make it reactive, we have to monitor updates to the editor and extract the string (.toString()) in bulk at every trigger. Furthermore, the redundant string would linger in the prop. Better implement it as a store to read (and also cache) the content on demand.

A "bindable" value is a nice-to-have if the user thinks the performance impact is tolerable. And this is more like a traditional <textarea>. CM 5 did have this feature via fromTextArea. The abovementioned store is pretty neat for our need. The "on demand" means that the extra work is hidden until the store has at least one subscription.

function editorTxHandler(tr) { this.update([tr]) /* ... */ if (tr.docChanged) { __docCached = null if (subscribers.size > 0) { // <--- updateDocStore(__docCached = tr.newDoc.toString()) } dispatch('change', {view: this, tr}) } }

TODO: How to handle the undo history when the store itself is updated?
If you try making a browser-native input field or textarea with ad-hoc text-editing commands, you will be surprised that the undo history is sometimes completely erased by a single programmatic update foo.value = 'bar' and there is no way to recover it.
But here, the default behavior will leave a trace each time the store is written. We can choose what to do with the undo stack CM provides.

Final design

https://svelte.dev/repl/91649ba3e0ce4122b3b34f3a95a00104?version=3.49.0