How to dynamically 'render' a string interweaved with `<span />`'s

I am attepting to make a text area where I can manually mark words, or sets of words, as either a spelling or grammatical error. in order to do this I switched from a plain <textarea /> to a <div contenteditable /> so that I have full control on how to render the text inside. Now my question is, the way I mark spelling and grammer errrors in with an array of index ranges ([number, number][]). right now I do the 'rendering' with this code
const html = createMemo(() => {
return value().split('').map((letter, index) => {
const spellingOpen = spellingErrors().some(([start]) => start === index) ? `<span class="${css.spellingError}">` : '';
const spellingClose = spellingErrors().some(([, end]) => end === index) ? `</span>` : '';

const grammarOpen = grammarErrors().some(([start]) => start === index) ? `<span class="${css.grammarError}">` : '';
const grammarClose = grammarErrors().some(([, end]) => end === index) ? `</span>` : '';

return `${grammarOpen}${spellingOpen}${letter}${spellingClose}${grammarClose}`;
}).join('');
});

return <div
contentEditable
innerHTML={html()}
/>;
const html = createMemo(() => {
return value().split('').map((letter, index) => {
const spellingOpen = spellingErrors().some(([start]) => start === index) ? `<span class="${css.spellingError}">` : '';
const spellingClose = spellingErrors().some(([, end]) => end === index) ? `</span>` : '';

const grammarOpen = grammarErrors().some(([start]) => start === index) ? `<span class="${css.grammarError}">` : '';
const grammarClose = grammarErrors().some(([, end]) => end === index) ? `</span>` : '';

return `${grammarOpen}${spellingOpen}${letter}${spellingClose}${grammarClose}`;
}).join('');
});

return <div
contentEditable
innerHTML={html()}
/>;
I however really dislike having to do manual string manipulation combined with the innterHTML. So the advice I would like to get. or otherwise brainstorming I would like to do. Is, how can I do this is a more solid-esc / jsx-esc way, so that I don't need to do manual string manipulation (and hopefully then also no longer having to rerender the content on every edit which resets the cursor to the start of the input).
9 Replies
zulu
zulu•3w ago
this seem tricky let say if you didn't need to edit and you needed to just mark words in an element how would you do it ? if you go the naive solution, you can break content by words, then wrap each word in an element ( so you can style it ) then you style the words that have the error. so this might be "simpler" to just render the problem is that the element can mutate and that can complicate things, but you can still sort of reapply the render over and over after every edit if there is any errors the non so solid way, will be to manipuate the dom of the div
Chris P Bacon
Chris P BaconOP•3w ago
that is basically the way I went about it now, only instead of by word I go by letter so that the indices match up. The problem I basically run into is that I do not know of a way in JSX where you only have an open tag. Which is totally logical, since it is 'just' a function call after all. I've tried to imaging a way where I somehow flatten my array of ranges into a flat array of indices. however I do not see a way on how I can then also differentiate on the type of index it is. If you think about it. what it is I am trying to do here is to un-flatten a tree if you reaaaaally boil it down or un-walk the tree if that's your preferred naming scheme 😛 also, I would like to expand this in the future to do rich-text editing as well (just basic markdown af far as I have in mind)
zulu
zulu•3w ago
essentially it boils down to text nodes. so i think i understand what you mean by un-flatten. and yes this is basically the objective. this is why I said if you go the always "render" then you simply remove all elements, retain just text. then re apply the wrappers of the text nodes that need to be wrapped. if you you allow rich formatting, and you need to retain that the problem becomes more complex. as you need to ignore existing nodes, consider text nodes only and add your wrappes so additional layer
Chris P Bacon
Chris P BaconOP•3w ago
I think you are on the money! I've actually for the most part managed to solve my issue by using the selection solid primitive. it is a really nice wrapper around the native selection API! Only issue I have left to solve is that I need to figure out what mutations to the text cause which mutations to the cursor position. i.e. when you insert 1 character the cursor should advance by 1.
zulu
zulu•3w ago
why do you need the position?
Chris P Bacon
Chris P BaconOP•3w ago
to make sure the behavior is as expected. All of this suff I'd happily skip, but the cursor gets reset to the start, since every rerender creates new nodes.
zulu
zulu•3w ago
oh got you, I guess the full rerender have its downsides. before you make the mutation take the cursor position after the rerender restore the curosr position you hook into the event that trigger the grammer check, and before the ui update 1.you store the cursor offset 2.do the wrapping 3. restore the cursor position based on the text offset create helpers to take the current text offset and create a helper to restore the cursor based on the given text offset
Chris P Bacon
Chris P BaconOP•3w ago
that is what I've done, which is also where the 'error' comes from. let's say I am at position x and I press a key in order to insert a letter. so before the insertion I store the cursor position, let the insert and rerender happen, then restore the cursor to pos x. well, that's wrong, because you are now at x+1. that's what I meant earlier. I realize now that I likely was not clear. I was sharing my thoughts, not asking a question. sorry for the confusion. I managed to solve the whole editor thing for the most part, pretty happy with where I ended up. Besides the spelling and grammar checking I also wanted to do bi-directional conversion between markdown and html. So I implemented a solution usinf unified with remark and rehype. and for error marking I created a plugin that adds the nodes to the ast instead of trying to insert it into the string as html. In case you are curious: https://github.com/chris-kruining/calque/blob/main/src/features/source/source.ts p.s. I'm going to close this question, it is straying way too far from it's original question.
zulu
zulu•3w ago
glad I could help, take the position after the input not before ( take position always after a user mutation ) after input you also get position caused by cut/paste also note that position can change without mutation such as mouse click or key press like arrow etc. and yes the markdown was not part of the original scope, perhaps the question scope changed and you are happy with everything i suggested

Did you find this page helpful?