A toolkit to create rich text editors in Elm

Hey all,

I’m proud to announce a new package for Elm for creating rich text editors!

Demo: https://mweiss.github.io/elm-rte-toolkit/
Package: https://package.elm-lang.org/packages/mweiss/elm-rte-toolkit/latest/
Github: https://github.com/mweiss/elm-rte-toolkit
Wiki: https://github.com/mweiss/elm-rte-toolkit/wiki

Background: About 4 months ago, I created a prototype rich text editor in Elm and discussed it here. With that feedback and some studying of other rich text editor frameworks, namely Trix, DraftJS, and most of all ProseMirror, I was able to get to v1 of this package.

In short, this is not a package to use as a drop in editor, but more a package to create your own custom rich text editor. A similar project for JavaScript is ProseMirror.

This package treats contenteditable as an I/O device, and uses browser events and mutation observers to detect changes and update itself. The editor’s model is defined and validated by a programmable specification that allows you to create a custom tailored editor that fits your needs.

I appreciate all packages, tools, and resources out there that the Elm community has created that helped me build this. If there’s interested, I’ll write more details about the implementation, like syncing contenteditable and the Elm virtual DOM, updating selection state, resolving events in a way that works across browser and platforms, the quirks with Android, and many more. I’d also appreciate any feedback you can give or interest in contributing to the project. Thanks!

42 Likes

This is a great contribution to the Elm ecosystem!

I think, this sort of stuff should be possible without additional JavaScript but I’m sure we will get there eventually.

1 Like

This is fantastic! I’m really happy to see this exists, especially after trying several more times myself in the last couple months. :laughing: I’m really looking forward to using this in an upcoming project!

2 Likes

I think it would, if Elm.Browser had bindings to Selection and MutationObserver.

1 Like

I’ve been hoping to see a ProseMirror-like package in Elm, so this is all very exciting to me, especially the fact that you can define your own custom schemas. I actually had one question related to that. So in ProseMirror one defines what children a particular node type can have through “content expressions”. It seems like you’re doing something pretty similar in Definitions with the way you set allowedGroups and allowedMarks.

One thing about “content expressions” in ProseMirror is that they not only allow you to determine which child nodes are allowed, but give you control over how those children are arranged and used. So if you have some container node that comprises paragraphs, you can stipulate that it must have at least one paragraph, or two paragraphs, or that the paragraph must come after a heading, etc. I was wondering if you had any plans or interest in adding such a feature in the future.

In any case, I’ll definitely be trying this out in a project very soon. Congrats on your first release!

1 Like

Thank you for the support and the feature suggestion.

I think adding something like content expressions would be great. I’ve added an issue to track this, along with an alternative proposal for maybe a more general validation method.

Awesome! I really look forward to using this package and seeing how it develops.

1 Like

Thank you for this! I’ve spent a lot of time trying to build my own based on the same idea with contenteditable as I/O, but I got lost in all the complexity. I greatly appreciate this!

1 Like

Great! The best I have been able to do so far is integrating Trix into an elm app, it works but customization and fine control are painful. Your solution looks much better! Looking forward to using it in my next project.

1 Like

This looks SO COOL! Thank you!

I have a project where I’d love a way to use Elm to make a custom in-browser code editor. Are there any particular limitations (maybe syntax highlighting is tricky or multi-select wouldn’t be possible or similar) that would prevent this same toolkit from being used to build a code editor rather than a rich text editor?

I think it’s possible to make a code editor with this, but there may be a few issues of performance and complexity that you’ll have to deal with eventually.

A code editor usually only cares about text, e.g. the data is really just text that’s turned into an AST later, and markup is more of a decoration added after the fact. If that’s the case for your editor, there are a lot of simplifications and performance optimizations you can do with an approach that doesn’t use contenteditable or a nested model, and instead uses a hidden textarea for input like Ace or Code Mirror. You can actually compare the differences in these approaches by looking at Prose Mirror and Code Mirror, both written by the same author but one is used for building rich text editors and the other as a versatile code editor. CodeMirror’s approach of using a hidden text area, simulating selection, and only rendering what’s visible is much more efficient, especially for larger documents. However, for a rich text editor which needs to be aware of its own markup and schema, CodeMirror’s approach is too basic.

I think the overlap of a rich text editor and a code editor comes in the form of a structural editor, e.g. an editor which is aware of its underlying structure. I think there would be more advantage in using this library for something like that.

Anyway, I hope that’s helpful, thank you for the support on the project and good luck with your in-browser code editor! I look forward to using it when comes out.

3 Likes

Speaking of performance, how much of a toll do you think Elm ports take? I’m especially worried about selection updates, which, I guess, can happen rather often as you drag your mouse.

Thanks for the question! Just for clarification, js to elm communication in the toolkit happens through webcomponents and custom events. I avoided ports because of the extra annoyance of making users copy port code for every project.

Custom selection events and change events from mutation observers are the majority of the messages processed by the editor. I haven’t benchmarked it or anything, but there is definitely some performance cost. That being said, selection state and text changes don’t usually trigger anything in the document to be updated, so they tend to be fast enough not to be noticeable in my testing*, even on large documents. The really inefficient thing I’ve noticed are commands like join and split in larger documents, because they trigger validation and reduction logic, which currently does a full scan of the entire editor state and dom. I’m hoping to make these more efficient as well some day, but it’s a much larger task to design, and may require some more development from the Elm APIs themselves.

*this was on a macbook, iOS simulator, iphone, and android emulator. The one exception where it was noticably laggy was on a Windows legacy VM for Edge, but actually every action I took, even outside the browser, was laggy so I don’t think it was a good test environment.

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.