How to change value of CodeMirror by TS code and HTMLElement referenced to it?

Let say I’ve some CodeMirror text, somethings like it:

<div id="foo-container"></div>
<div id="bar-container"></div>

which I create as:

create(elem: HTMLElement) {
	if (!elem) return;

	const customTheme = EditorView.theme({
		".cm-content": {
			fontFamily: "var(--bs-body-font-family) !important"
		}
	});
	let state = EditorState.create({
		doc: "",
		extensions: [
			keymap.of([...defaultKeymap, indentWithTab]),
			bracketMatching(),
			closeBrackets(),
			customTheme
		]
	});

	let view = new EditorView({
		state,
		parent: elem
	});
}

create(document.getElementById("foo-container") as HTMLElement);
create(document.getElementById("bar-container") as HTMLElement);

Now, I need to change its value(displayed so) by HTMLElement id, such as:

function updateElementFromData(element: HTMLElement, value: string): void {
	console.log(element); // this will print the html element foo or bar
	// here's the logic to update code mirror value by element
}

Is it possible? How can I do it? Of course updateElementFromData must be agnostic, and retrieve the object by HTMLElement.

Such as if the HTMLElement was an instanceof of HTMLTextAreaElement, I can easily do:

element.value = value.toString();

Thanks

The EditorView.findFromDOM static method may be useful for getting an EditorView instance from a DOM element. To set a new document, you want to create a new EditorState and use EditorView.setState.

1 Like

@marijn wow, what a serious tool. Seems to works like a charm:

let customTextArea = EditorView.findFromDOM(element);
if (customTextArea) {
	const state = CustomTextArea.createState(value.toString());
	customTextArea.setState(state);
}

Since it seems I can’t change the actual .doc variable, I’ve created the helper createState (which has default state for every object). Thanks.

Now: how can I do the opposite? I mean, from an HTMLElement element:

  1. understand if that’s an instance of EditorView
  2. if its, attach an addEventListener, so any edits of that “doc”, will trigger a specific function?

Is it also possible?

See EditorView.updateListener, and view.state.doc.toString() to get the document as a string.

mmm, not sure, but how can I attach an update listener over a already rendered CodeMirror?

Such as:

let customTextArea = EditorView.findFromDOM(element);
if (customTextArea) {
	// how can I attach updateListener to customTextArea?
}

Tried this:

let customTextArea = EditorView.findFromDOM(element);
if (customTextArea) {
	const updateListener = EditorView.updateListener.of((update) => {
		if (update.docChanged) {
			console.log("Document changed:", update.state.doc.toString());
		}
	});

	const reconfigureEffect = StateEffect.appendConfig.of(updateListener);
	customTextArea.dispatch({
		effects: reconfigureEffect,
	});
}

but when I write somethings on CodeMirror, can’t see any logs…

You could dispatch a reconfiguration transaction, but in general, the preferred style for CodeMirror is to configure the state when creating it, rather than via imperative updates.

Tried also this, somethigs like this:

let customTextArea = EditorView.findFromDOM(element);
if (customTextArea) {
	const updateListener = EditorView.updateListener.of((update) => {
		console.log("Document changed:", update.docChanged);

		if (update.docChanged) {
			const newContent = update.state.doc.toString();
			console.log("New content:", newContent);
		}
	});

	customTextArea.dispatch({
		effects: StateEffect.reconfigure.of(updateListener)
	});
}

still can’t see any console log :frowning:

Neither with appendConfig:

let customTextArea = EditorView.findFromDOM(element);
if (customTextArea) {
	const updateListener = EditorView.updateListener.of((update) => {
		console.log("Update occurred!");

		if (update.docChanged) {
			const newContent = update.state.doc.toString();
			console.log("Document changed to:", newContent);
		}
	});

	customTextArea.dispatch({
		effects: StateEffect.appendConfig.of(updateListener)
	});
}

Where am I wrong?

I don’t know. This works fine for me.

I got what’s the problem. IS because on other part I’ve the first part of code:

let customTextArea = EditorView.findFromDOM(element);
if (customTextArea) {
	const state = CustomTextArea.createState(value.toString());
	customTextArea.setState(state);
}

which run later and override the previous update handler :slight_smile:

SO: how can I update the “doc” text when change, instead of create a new EditorState and set it? (which is basically HUGE in terms of performances, since it will override template, indent, and N params).

I believe there is somethings straightforward to update doc programmatically?

@marijn maybe this way? (find on internet)

let customTextArea = EditorView.findFromDOM(element);
if (customTextArea) {
	customTextArea.dispatch({
		changes: {
			from: 0,
			to: customTextArea.state.doc.length,
			insert: value.toString()
		}
	});
}

is it the correct way?

That works, but it won’t clear the undo history, so if you’re loading a new document that’s probably not what you want.

What do you mean with “load new document”?
Copy/paste seems to works… Example?

@marijn ok, I start to see what are the problems with history.

Here’s my actual updateContent function, which programmatically change the text of CodeMirror (useful because I’m using it on a DataBind to a my custom JS object):

static updateContent(view: EditorView, value: string) {
	view.dispatch({
		changes: {
			from: 0,
			to: view.state.doc.length,
			insert: value
		},
		selection: EditorSelection.cursor(Math.min(view.state.selection.main.head, value.length)), // preserve cursor after change content
		annotations: CustomTextArea.annotationDataBind.of(true) // prevent infinite loop when set value on DataBind (avoid to trigger updateListener)
	});
}

History seems to works (I’m using it like this):

// state
const state = EditorState.create({
	doc: `function test() {\n    console.log("Hello, CodeMirror!");\n}`,
	extensions: [
		keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
		history(),
		bracketMatching(),
		closeBrackets(),
		indentUnit.of("    "),
		theme,
		CustomTextArea.highlightField
	]
});

// view
const view = new EditorView({
	state,
	parent: elem
});

but seems I can only “undo” 1 step? How can I keep the history for all undo in this case?

You can (try it in the demo editor on the project website). Though of course if you keep recreating states you’ll lose your old history.

Sorry, don’t follow this.

I’m not creating a new State, I simply call

view.dispatch({
		changes: {
			from: 0,
			to: view.state.doc.length,
			insert: value
		},
		selection: EditorSelection.cursor(Math.min(view.state.selection.main.head, value.length)), // preserve cursor after change content
		annotations: CustomTextArea.annotationDataBind.of(true) // prevent infinite loop when set value on DataBind (avoid to trigger updateListener)
	});

which update the value, nothing more. But it preserve only the last change. So history (ctrl+z) just restore a single undo.

i.e. if I wrote text, and do a new line, with ctrl+z I can just restore One/unique single undo (not as many as I Need as for usual text box pressing many ctrl+z).

Why? Where am I wrong?

If you do this closer than half a second together, the changes (since they overlap—because you replace the entire document) will be combined into a single event, and you’ll undo them all at once. If that’s not it, I don’t know what the issue is.

Nope. If I:

digit A
Wait 2 seconds
digit B
Wait 2 seconds
digit C
Wait 3 seconds

I end up with ABC

Now if I do undo (ctrl+z) it becomes AB, than stop: I can report undo, nothing change :frowning:

@marijn here you go with a full example

Try write some text, wait also seconds between every hits, and than undo… undo… undo… only the first will work.

Basically I need an external “watcher” which will be handled on change, thus update a linked “member” variable of an object: so sync the change between che CodeMirror and my own custom object on change.

I guess this is the way on doing this? But not sure why I’m loosing the history :frowning:

What appears to be happening is that your redoing of every change (with a full content replace) keeps re-adding an event to the undo history whenever you’re trying to undo, so that the next undo just undoes that (useless) replace.

@marijn is this considered a sort of “bug” so?

If I replace with an entire text, its still a replace/change.

So pass from whole test “a” => “aa” => “aaa”, each undo must return “aa” => “a” (even if its a whole text replace).

The same on what happens on a textarea if I select all, replace with another text+a for N times.

Right?