This post covers how I used the contenteditable
attribute in Ember and worked around a jumping cursor issue.
My First Implementation with a Jumping Cursor
I started with an implementation that adhered to Data Down, Actions Up (DDAU):
<div contenteditable="true" >
</div>
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class ApplicationController extends Controller {
@tracked content = 'foo';
@action
setContent(event) {
this.content = event.target.textContent;
}
}
It worked, but when I typed into the contenteditable div
, the cursor jumped to the beginning of the element after each keystroke, which is a terrible user experience.
The Solution
To fix this, I created a modifier called contenteditable
:
ember install ember-modifier
ember g modifier contenteditable
Here is how the contenteditable
modifier can be used:
<div ></div>
And here is the modifier's implementation:
import { modifier } from 'ember-modifier';
export default modifier(function contenteditable(element, positional, named) {
element.contentEditable = true;
if (named.textContent !== element.textContent) {
element.textContent = named.textContent;
}
function handleInput(event) {
named.onInput(event.target.textContent);
}
element.addEventListener('input', handleInput);
return () => {
element.removeEventListener('input', handleInput);
};
});
The implementation is pretty straightforward. It does the following:
- Adds the
contenteditable
attribute to the element that the modifier was invoked on - Adds an event listener for the input event and invokes the
onInput
argument with the element'stextContent
- Removes the input event listener in the modifier's teardown.
Per the ember-modifier
documentation regarding modifiers:
Any tracked values that it accesses will be tracked, including the arguments it receives, and if any of them changes, the function will run again.
So whenever this.content
changes, the modifier will run again. When that happens, the cursor jumps to the start of the contenteditable div
because the textContent
of the div
is reassigned.
To get around this, I added the following conditional:
if (named.textContent !== element.textContent) {
element.textContent = named.textContent;
}
As a user types into the contenteditable div
, named.textContent
and element.textContent
will be the same value, so the element's textContent
won't get reassigned which caused the jumping cursor issue. The only time these two values might not be the same is on initial render where I specified an initial value of "foo" and the element's initial textContent
is an empty string. Even if there isn't an initial value, and hence the values will be the same (an empty string), there won't be a problem as the cursor will just start at the beginning of the element.
Now it works as expected: